From f2b2601b236b9bba305ec924778e7203ca928c2b Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Sun, 18 Oct 2026 17:23:37 -0600 Subject: [PATCH 1/6] fix: contain unowned Hive launch and repair mailbox regressions --- CONTRIBUTING.md | 6 + docs/audits/2026-10-18-hive-repair/GUIA.md | 74 + docs/security/m15-authority-coverage.json | 4604 ++++++++++---------- src/isycode/cat_activity.py | 125 +- src/isycode/hive/__init__.py | 8 +- src/isycode/hive/guard.py | 15 +- src/isycode/hive/hook_shim.py | 30 +- src/isycode/hive/manager.py | 152 +- src/isycode/hive/router.py | 57 +- src/isycode/hive/safe_io.py | 72 + src/isycode/hive/spawn.py | 90 +- src/isycode/hive/types.py | 38 +- src/isycode/tui_app_sessions.py | 55 +- src/isycode/tui_screens_harness.py | 4 +- tests/test_cat_activity.py | 10 +- tests/test_hive_regressions.py | 201 + tests/test_session_continuity_tui.py | 2 +- 17 files changed, 2902 insertions(+), 2641 deletions(-) create mode 100644 docs/audits/2026-10-18-hive-repair/GUIA.md create mode 100644 src/isycode/hive/safe_io.py create mode 100644 tests/test_hive_regressions.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af91f14..008d2dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,12 @@ La suite hermética se ejecuta con: python -m pytest -q -m "not integration" ``` +Si el checkout no está instalado en el intérprete actual, ejecuta +`PYTHONPATH=src python -m pytest -q -m "not integration"` en Linux/macOS. +El `pythonpath` de pytest no se propaga automáticamente a procesos hijos; +una instalación de otro checkout puede causar errores de importación engañosos. +La instalación editable en un entorno virtual sigue siendo la ruta recomendada. + Las pruebas marcadas `integration` pueden hablar con servicios reales. Revisa el test y su documentación antes de habilitar una integración externa. diff --git a/docs/audits/2026-10-18-hive-repair/GUIA.md b/docs/audits/2026-10-18-hive-repair/GUIA.md new file mode 100644 index 0000000..e220681 --- /dev/null +++ b/docs/audits/2026-10-18-hive-repair/GUIA.md @@ -0,0 +1,74 @@ +# Reparación del Hive y del gato — 2026-10-18 + +Esta corrección parte de `594050e29b2bbd57022efee5ad56eced275cfca7`. + +## Cómo usar la reparación de harnesses + +Abre Multi Harness, revisa el compose y elige **Launch repair conversation**. +Se abre una conversación normal de ISyCode con el modelo seleccionado en la +sesión. El compose completo se publica como mensaje; los permisos y las +aprobaciones siguen pasando por los owners habituales, Authority e ISySentinel. + +**External Hive · unavailable** está deshabilitado. El lanzamiento anterior +arrancaba un CLI separado sin owner de ejecución y descartaba el compose. +No basta con corregir su selector: los hooks de coordinación no controlan los +comandos ni las acciones de ese proceso. La API de lanzamiento también rechaza +la operación sin crear agentes, arrancar sockets ni ejecutar procesos. + +El buzón Hive sigue siendo experimental. Sus correcciones incluyen validación +de identificadores y sobres, remitente ligado al outbox, rechazo de enlaces, +lectura limitada a archivos regulares, entrega sin sobrescribir y acuse en +`.done`. El router ya no relee `.sent` y consulta el registro vigente. +Los archivos rechazados se conservan; no se eliminan ni se mueven al intentar +validar una ruta insegura. + +El generador experimental de configuración Claude invoca el módulo Python +existente, con rutas entrecomilladas. No genera hooks ni flags de bypass para +otros CLIs. La prueba ejecuta ese shim localmente con un evento controlado; +no demuestra una sesión real de Claude ni autoriza lanzar un proceso externo. + +## Gato + +El gato ahora es naranja, de contorno ligero, cuerpo compacto y cola levantada +al caminar. Dormido conserva la silueta y una cola más visible. Si caben, muestra +tres `z` en escalera; en columnas estrechas conserva la presentación compacta. +Los puntos siguen usando Braille Unicode de 2×4 subceldas. El tamaño físico de +cada punto depende de la fuente del terminal, que este cambio no modifica. +La animación y el modo de movimiento reducido conservan su lógica existente. + +## Verificación local + +Usa el entorno de desarrollo descrito en `CONTRIBUTING.md`. Si ejecutas desde +un checkout que no está instalado en el intérprete actual, propaga su fuente a +los procesos hijos para evitar que importen otra instalación: + +```bash +PYTHONPATH=src python -m pytest -q tests/test_hive.py tests/test_hive_regressions.py tests/test_cat_activity.py +PYTHONPATH=src python -m pytest -q -m 'not integration' --tb=short +``` + +El snapshot M15 se regenera desde `authority_coverage_snapshot()`; no se +modifican sus clasificaciones a mano. Su coincidencia con el catálogo no +certifica por sí sola un nuevo subsistema ni una integración externa. + +El test de cancelación de compactación espera `wait_for_ready()` antes de +persistir la sesión: una pausa de render no garantiza que el owner exista. +Esto elimina esa dependencia temporal del test. No se atribuye retroactivamente +una causa demostrada al fallo histórico intermitente de CI. + +## Protecciones de GitHub + +Se verificó y actualizó la protección de `main`: exige la base actualizada, +Linux, Linux con Textual 8, Windows y Gate evidence. También se aplica a +administradores. No se reescribió el historial ni se fusionó ningún PR. + +## Límites pendientes + +- Owner de ejecución acotado para CLIs externos Hive: pendiente; lanzamiento bloqueado. +- Revisión de protocolos de hooks para otros CLIs: pendiente, no se anuncia soporte. +- Pruebas nativas Windows de estos cambios y CI remoto: no demostradas por una ejecución Linux local. +- Resistencia completa a carreras de filesystem en Windows: no demostrada; el + helper POSIX fija descriptores de directorio, Windows rechaza enlaces y junctions + observables pero no proporciona una garantía equivalente frente a procesos hostiles. +- Fuente y apariencia en tu terminal físico: la comparación raster y las + capturas Textual son vistas previas; no cambian ni certifican tu fuente. diff --git a/docs/security/m15-authority-coverage.json b/docs/security/m15-authority-coverage.json index ba308f5..f51ebe4 100644 --- a/docs/security/m15-authority-coverage.json +++ b/docs/security/m15-authority-coverage.json @@ -1,2305 +1,2305 @@ { - "format": "isycode.m15-authority-coverage.v1", - "summary": { - "actions": 84, - "effectful": 63, - "owner_valid": 57, - "owner_shared_read": 1, - "owner_variants": 1, - "explicit_deny": 24, - "secure_excluded": 1, - "unclassified": 0, - "ambiguous": 0, - "authority_frontier_pass": true, - "secure_tui_closed": true, - "secure_closed": true - }, - "actions": [ - { - "action": "workspace.files.list", - "effect": "read", - "approval_required": false, - "owners": [ - "workspace_read" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "NOT_DEMONSTRATED" - }, - { - "action": "workspace.files.read", - "effect": "read", - "approval_required": false, - "owners": [ - "broker_preview", - "broker_provision", - "grit_peers", - "lsp_symbols", - "workspace_read" - ], - "classification": "OWNER_SHARED_READ", - "authority": "Workspace Authority", - "sentinel": [ - "BrokerProvisionBoundary", - "BrokerRecipeBoundary", - "BrokerRegistryBoundary", - "GritPeersBoundary", - "LSPProcessBoundary", - "WorkspaceReadBoundary" - ], - "variants": [], - "status": "OWNER_SHARED_READ", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.files.search", - "effect": "read", - "approval_required": false, - "owners": [ - "workspace_read" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "NOT_DEMONSTRATED" - }, - { - "action": "workspace.context.inject", - "effect": "read", - "approval_required": false, - "owners": [ - "workspace_read" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.config.read", - "effect": "read", - "approval_required": false, - "owners": [ - "workspace_config" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceConfigBoundary", - "WorkspaceReadBoundary", - "WorkspaceWriteBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.config.list", - "effect": "read", - "approval_required": false, - "owners": [ - "workspace_config" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceConfigBoundary", - "WorkspaceReadBoundary", - "WorkspaceWriteBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.config.write", - "effect": "write", - "approval_required": true, - "owners": [ - "workspace_config" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceConfigBoundary", - "WorkspaceReadBoundary", - "WorkspaceWriteBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.files.write", - "effect": "write", - "approval_required": true, - "owners": [ - "workspace_write" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceWriteBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.files.restore", - "effect": "write", - "approval_required": true, - "owners": [ - "workspace_write" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceWriteBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.command.run", - "effect": "process", - "approval_required": true, - "owners": [ - "workspace_command" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "CommandProcessBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "git.status", - "effect": "read", - "approval_required": false, - "owners": [ - "workspace_git" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "GitBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "git.diff", - "effect": "read", - "approval_required": false, - "owners": [ - "workspace_git" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "GitBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "git.commit", - "effect": "write", - "approval_required": true, - "owners": [ - "workspace_git" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "GitBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "git.push", - "effect": "external", - "approval_required": true, - "owners": [ - "workspace_publish" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "PublishBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.files.move", - "effect": "write", - "approval_required": true, - "owners": [ - "workspace_write" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceWriteBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.files.delete", - "effect": "destructive", - "approval_required": true, - "owners": [ - "workspace_write" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceWriteBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.files.read_sensitive", - "effect": "sensitive", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "provider.authenticate", - "effect": "credential", - "approval_required": true, - "owners": [ - "provider_auth" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "ProviderAuthBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "provider.request", - "effect": "network", - "approval_required": false, - "owners": [ - "provider_network" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "ProviderNetworkBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "grok.login", - "effect": "process", - "approval_required": true, - "owners": [ - "grok_auth" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "GrokAuthBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "web.fetch", - "effect": "network-read", - "approval_required": false, - "owners": [ - "web_fetch" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "RemoteReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "catalog.external.read", - "effect": "network-read", - "approval_required": false, - "owners": [ - "remote_catalog" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "RemoteReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "NOT_DEMONSTRATED" - }, - { - "action": "gateway.semantic.read", - "effect": "network-read", - "approval_required": true, - "owners": [ - "gateway_semantic" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "GatewaySemanticBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "gateway.files.read", - "effect": "network-read", - "approval_required": false, - "owners": [ - "remote_catalog" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "RemoteReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "NOT_DEMONSTRATED" - }, - { - "action": "gateway.files.write", - "effect": "network-write", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "oauth.authorize", - "effect": "credential", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "credentials.add", - "effect": "credential", - "approval_required": true, - "owners": [ - "credentials" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "CredentialBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "credentials.use", - "effect": "credential", - "approval_required": false, - "owners": [ - "credential_use" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "CredentialBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "credentials.revoke", - "effect": "destructive", - "approval_required": true, - "owners": [ - "credentials" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "CredentialBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "mcp.discover", - "effect": "network-read", - "approval_required": false, - "owners": [ - "remote_catalog" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "RemoteReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "NOT_DEMONSTRATED" - }, - { - "action": "mcp.invoke", - "effect": "external", - "approval_required": true, - "owners": [ - "gateway_mcp" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "MCPInvocationBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "mcp.local.start", - "effect": "process", - "approval_required": true, - "owners": [ - "mcp_local" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "LocalMCPBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "mcp.local.invoke", - "effect": "external", - "approval_required": true, - "owners": [ - "mcp_local" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "LocalMCPBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "lsp.discover", - "effect": "read", - "approval_required": false, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "lsp.start", - "effect": "process", - "approval_required": true, - "owners": [ - "lsp_symbols" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "LSPProcessBoundary", - "WorkspaceReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "lsp.stop", - "effect": "process", - "approval_required": false, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "lsp.diagnostics", - "effect": "process", - "approval_required": false, - "owners": [ - "lsp_symbols" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "LSPProcessBoundary", - "WorkspaceReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "grit.discover", - "effect": "read", - "approval_required": false, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "grit.claims.read", - "effect": "read", - "approval_required": false, - "owners": [ - "grit_peers" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "GritPeersBoundary", - "WorkspaceReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "broker.preview", - "effect": "read", - "approval_required": false, - "owners": [ - "broker_preview" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "BrokerRecipeBoundary", - "WorkspaceReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "NOT_DEMONSTRATED" - }, - { - "action": "broker.build", - "effect": "process", - "approval_required": true, - "owners": [ - "broker_provision" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "BrokerProvisionBoundary", - "BrokerRegistryBoundary", - "WorkspaceReadBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "broker.start", - "effect": "process", - "approval_required": true, - "owners": [ - "broker_management", - "broker_provision" - ], - "classification": "OWNER_VARIANTS", - "authority": "Workspace Authority", - "sentinel": [ - "BrokerProvisionBoundary", - "BrokerRegistryBoundary", - "WorkspaceReadBoundary" - ], - "variants": [ - { - "owner": "broker_provision", - "requires": { - "operation": "start" - }, - "absent": [ - "managed_existing" - ] - }, - { - "owner": "broker_management", - "requires": { - "operation": "start", - "managed_existing": true - }, - "absent": [] + "format": "isycode.m15-authority-coverage.v1", + "summary": { + "actions": 84, + "effectful": 63, + "owner_valid": 57, + "owner_shared_read": 1, + "owner_variants": 1, + "explicit_deny": 24, + "secure_excluded": 1, + "unclassified": 0, + "ambiguous": 0, + "authority_frontier_pass": true, + "secure_tui_closed": true, + "secure_closed": true + }, + "actions": [ + { + "action": "workspace.files.list", + "effect": "read", + "approval_required": false, + "owners": [ + "workspace_read" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "NOT_DEMONSTRATED" + }, + { + "action": "workspace.files.read", + "effect": "read", + "approval_required": false, + "owners": [ + "broker_preview", + "broker_provision", + "grit_peers", + "lsp_symbols", + "workspace_read" + ], + "classification": "OWNER_SHARED_READ", + "authority": "Workspace Authority", + "sentinel": [ + "BrokerProvisionBoundary", + "BrokerRecipeBoundary", + "BrokerRegistryBoundary", + "GritPeersBoundary", + "LSPProcessBoundary", + "WorkspaceReadBoundary" + ], + "variants": [], + "status": "OWNER_SHARED_READ", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.files.search", + "effect": "read", + "approval_required": false, + "owners": [ + "workspace_read" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "NOT_DEMONSTRATED" + }, + { + "action": "workspace.context.inject", + "effect": "read", + "approval_required": false, + "owners": [ + "workspace_read" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.config.read", + "effect": "read", + "approval_required": false, + "owners": [ + "workspace_config" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceConfigBoundary", + "WorkspaceReadBoundary", + "WorkspaceWriteBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.config.list", + "effect": "read", + "approval_required": false, + "owners": [ + "workspace_config" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceConfigBoundary", + "WorkspaceReadBoundary", + "WorkspaceWriteBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.config.write", + "effect": "write", + "approval_required": true, + "owners": [ + "workspace_config" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceConfigBoundary", + "WorkspaceReadBoundary", + "WorkspaceWriteBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.files.write", + "effect": "write", + "approval_required": true, + "owners": [ + "workspace_write" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceWriteBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.files.restore", + "effect": "write", + "approval_required": true, + "owners": [ + "workspace_write" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceWriteBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.command.run", + "effect": "process", + "approval_required": true, + "owners": [ + "workspace_command" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "CommandProcessBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "git.status", + "effect": "read", + "approval_required": false, + "owners": [ + "workspace_git" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "GitBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "git.diff", + "effect": "read", + "approval_required": false, + "owners": [ + "workspace_git" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "GitBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "git.commit", + "effect": "write", + "approval_required": true, + "owners": [ + "workspace_git" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "GitBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "git.push", + "effect": "external", + "approval_required": true, + "owners": [ + "workspace_publish" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "PublishBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.files.move", + "effect": "write", + "approval_required": true, + "owners": [ + "workspace_write" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceWriteBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.files.delete", + "effect": "destructive", + "approval_required": true, + "owners": [ + "workspace_write" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceWriteBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.files.read_sensitive", + "effect": "sensitive", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "provider.authenticate", + "effect": "credential", + "approval_required": true, + "owners": [ + "provider_auth" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "ProviderAuthBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "provider.request", + "effect": "network", + "approval_required": false, + "owners": [ + "provider_network" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "ProviderNetworkBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "grok.login", + "effect": "process", + "approval_required": true, + "owners": [ + "grok_auth" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "GrokAuthBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "web.fetch", + "effect": "network-read", + "approval_required": false, + "owners": [ + "web_fetch" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "RemoteReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "catalog.external.read", + "effect": "network-read", + "approval_required": false, + "owners": [ + "remote_catalog" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "RemoteReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "NOT_DEMONSTRATED" + }, + { + "action": "gateway.semantic.read", + "effect": "network-read", + "approval_required": true, + "owners": [ + "gateway_semantic" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "GatewaySemanticBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "gateway.files.read", + "effect": "network-read", + "approval_required": false, + "owners": [ + "remote_catalog" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "RemoteReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "NOT_DEMONSTRATED" + }, + { + "action": "gateway.files.write", + "effect": "network-write", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "oauth.authorize", + "effect": "credential", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "credentials.add", + "effect": "credential", + "approval_required": true, + "owners": [ + "credentials" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "CredentialBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "credentials.use", + "effect": "credential", + "approval_required": false, + "owners": [ + "credential_use" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "CredentialBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "credentials.revoke", + "effect": "destructive", + "approval_required": true, + "owners": [ + "credentials" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "CredentialBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "mcp.discover", + "effect": "network-read", + "approval_required": false, + "owners": [ + "remote_catalog" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "RemoteReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "NOT_DEMONSTRATED" + }, + { + "action": "mcp.invoke", + "effect": "external", + "approval_required": true, + "owners": [ + "gateway_mcp" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "MCPInvocationBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "mcp.local.start", + "effect": "process", + "approval_required": true, + "owners": [ + "mcp_local" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "LocalMCPBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "mcp.local.invoke", + "effect": "external", + "approval_required": true, + "owners": [ + "mcp_local" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "LocalMCPBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "lsp.discover", + "effect": "read", + "approval_required": false, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "lsp.start", + "effect": "process", + "approval_required": true, + "owners": [ + "lsp_symbols" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "LSPProcessBoundary", + "WorkspaceReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "lsp.stop", + "effect": "process", + "approval_required": false, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "lsp.diagnostics", + "effect": "process", + "approval_required": false, + "owners": [ + "lsp_symbols" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "LSPProcessBoundary", + "WorkspaceReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "grit.discover", + "effect": "read", + "approval_required": false, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "grit.claims.read", + "effect": "read", + "approval_required": false, + "owners": [ + "grit_peers" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "GritPeersBoundary", + "WorkspaceReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "broker.preview", + "effect": "read", + "approval_required": false, + "owners": [ + "broker_preview" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "BrokerRecipeBoundary", + "WorkspaceReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "NOT_DEMONSTRATED" + }, + { + "action": "broker.build", + "effect": "process", + "approval_required": true, + "owners": [ + "broker_provision" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "BrokerProvisionBoundary", + "BrokerRegistryBoundary", + "WorkspaceReadBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "broker.start", + "effect": "process", + "approval_required": true, + "owners": [ + "broker_management", + "broker_provision" + ], + "classification": "OWNER_VARIANTS", + "authority": "Workspace Authority", + "sentinel": [ + "BrokerProvisionBoundary", + "BrokerRegistryBoundary", + "WorkspaceReadBoundary" + ], + "variants": [ + { + "owner": "broker_provision", + "requires": { + "operation": "start" + }, + "absent": [ + "managed_existing" + ] + }, + { + "owner": "broker_management", + "requires": { + "operation": "start", + "managed_existing": true + }, + "absent": [] + } + ], + "status": "OWNER_VARIANTS", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "broker.health", + "effect": "read", + "approval_required": false, + "owners": [ + "broker_management" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "BrokerRegistryBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "NOT_DEMONSTRATED" + }, + { + "action": "broker.logs", + "effect": "read", + "approval_required": true, + "owners": [ + "broker_management" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "BrokerRegistryBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "NOT_DEMONSTRATED" + }, + { + "action": "broker.stop", + "effect": "process", + "approval_required": true, + "owners": [ + "broker_management" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "BrokerRegistryBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "NOT_DEMONSTRATED" + }, + { + "action": "broker.remove", + "effect": "destructive", + "approval_required": true, + "owners": [ + "broker_management" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "BrokerRegistryBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "NOT_DEMONSTRATED" + }, + { + "action": "mobile.host.start", + "effect": "network", + "approval_required": true, + "owners": [ + "mobile_host" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "MobileHostBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "mobile.host.stop", + "effect": "process", + "approval_required": false, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "mobile.pair", + "effect": "credential", + "approval_required": false, + "owners": [ + "mobile_host" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "MobileHostBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "mobile.pair.issue", + "effect": "credential", + "approval_required": true, + "owners": [ + "mobile_host" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "MobileHostBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "mobile.session.read", + "effect": "read", + "approval_required": false, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "mobile.session.create", + "effect": "external", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "mobile.session.cancel", + "effect": "external", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "mobile.approval.respond", + "effect": "external", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "bridge.connect", + "effect": "network", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "bridge.peek", + "effect": "read", + "approval_required": false, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "bridge.send", + "effect": "external", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "bridge.lease.claim", + "effect": "coordination", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "bridge.lease.release", + "effect": "coordination", + "approval_required": false, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "bridge.wake", + "effect": "external", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "bridge.agents", + "effect": "read", + "approval_required": true, + "owners": [ + "bridge_presence" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "BridgePresenceBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "l1.create", + "effect": "write", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "l1.validate", + "effect": "process", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "l1.test", + "effect": "process", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "l1.activate", + "effect": "write", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "l1.disable", + "effect": "write", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "l1.rollback", + "effect": "write", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "session.create", + "effect": "write", + "approval_required": false, + "owners": [ + "chat_sessions" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "SessionStoreBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "session.resume", + "effect": "read", + "approval_required": false, + "owners": [ + "chat_sessions" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "SessionStoreBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "session.delete", + "effect": "destructive", + "approval_required": true, + "owners": [ + "session_delete" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "SessionDeleteBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.memory.read", + "effect": "read", + "approval_required": false, + "owners": [ + "workspace_memory" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceMemoryBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.memory.write", + "effect": "write", + "approval_required": true, + "owners": [ + "workspace_memory" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceMemoryBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "workspace.memory.forget", + "effect": "destructive", + "approval_required": true, + "owners": [ + "workspace_memory" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "WorkspaceMemoryBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "clipboard.paste", + "effect": "external", + "approval_required": false, + "owners": [ + "clipboard" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "ClipboardBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "clipboard.copy", + "effect": "external", + "approval_required": false, + "owners": [ + "clipboard" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "ClipboardBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "desktop.file_picker", + "effect": "process", + "approval_required": true, + "owners": [], + "classification": "EXPLICIT_DENY", + "authority": "explicit deny at owner binding", + "sentinel": [], + "variants": [], + "status": "EXPLICIT_DENY", + "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" + }, + { + "action": "tailscale.inspect", + "effect": "read", + "approval_required": false, + "owners": [ + "tailscale_read" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "TailscaleExecutableBoundary", + "TailscaleGatewayBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "tailscale.install.prepare", + "effect": "process", + "approval_required": true, + "owners": [ + "tailscale_package_install" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "TailscalePackageBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "tailscale.install.stage", + "effect": "process", + "approval_required": true, + "owners": [ + "tailscale_package_install" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "TailscalePackageBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "tailscale.install", + "effect": "process", + "approval_required": true, + "owners": [ + "tailscale_package_install" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "TailscalePackageBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "tailscale.login", + "effect": "process", + "approval_required": true, + "owners": [ + "tailscale_login" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "TailscaleExecutableBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "tailscale.serve.enable", + "effect": "process", + "approval_required": true, + "owners": [ + "tailscale_serve" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "TailscaleExecutableBoundary", + "TailscaleGatewayBoundary", + "TailscalePrivateServeBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "tailscale.serve.disable", + "effect": "process", + "approval_required": true, + "owners": [ + "tailscale_serve" + ], + "classification": "OWNER_VALID", + "authority": "Workspace Authority", + "sentinel": [ + "TailscaleExecutableBoundary", + "TailscaleGatewayBoundary", + "TailscalePrivateServeBoundary" + ], + "variants": [], + "status": "OWNER_VALID", + "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" + }, + { + "action": "role.select", + "effect": "read", + "approval_required": false, + "owners": [], + "classification": "SECURE_EXCLUDED", + "authority": "not an effect authority action", + "sentinel": [], + "variants": [], + "status": "SECURE_EXCLUDED", + "secure_reachability": "NOT_AN_EFFECT_ACTION" + } + ], + "owner_action_mismatches": [], + "owner_contract_gaps": [], + "variant_contract_gaps": [], + "ambiguous_actions": [], + "unclassified_actions": [], + "callsites": [ + { + "action": "web.fetch", + "callsite": "WebFetchOwner.execute", + "owner": "web_fetch", + "status": "COVERED" + }, + { + "action": "session.create", + "callsite": "IterationOwner._persist", + "owner": "chat_sessions", + "status": "COVERED" + }, + { + "action": "session.resume", + "callsite": "IterationOwner.inspect", + "owner": "chat_sessions", + "status": "COVERED" + }, + { + "action": "provider.request", + "callsite": "run_iteration.complete", + "owner": "provider_network", + "status": "COVERED" + }, + { + "action": "tailscale.inspect", + "callsite": "TailscaleReadOwner.inspect", + "owner": "tailscale_read", + "status": "COVERED" + }, + { + "action": "tailscale.install.prepare", + "callsite": "TailscalePackageInstallOwner.prepare", + "owner": "tailscale_package_install", + "status": "COVERED" + }, + { + "action": "tailscale.install.stage", + "callsite": "TailscalePackageInstallOwner.stage", + "owner": "tailscale_package_install", + "status": "COVERED" + }, + { + "action": "tailscale.install", + "callsite": "TailscalePackageInstallOwner.install", + "owner": "tailscale_package_install", + "status": "COVERED" + }, + { + "action": "tailscale.login", + "callsite": "TailscaleLoginOwner.begin_login", + "owner": "tailscale_login", + "status": "COVERED" + }, + { + "action": "tailscale.serve.enable", + "callsite": "TailscaleServeOwner.enable", + "owner": "tailscale_serve", + "status": "COVERED" + }, + { + "action": "tailscale.serve.disable", + "callsite": "TailscaleServeOwner.disable", + "owner": "tailscale_serve", + "status": "COVERED" + }, + { + "action": "provider.request", + "callsite": "ProviderNetworkOwner.execute", + "owner": "provider_network", + "status": "COVERED" + }, + { + "action": "provider.authenticate", + "callsite": "ProviderAuthOwner.begin", + "owner": "provider_auth", + "status": "COVERED" + }, + { + "action": "grok.login", + "callsite": "GrokAuthOwner.begin", + "owner": "grok_auth", + "status": "COVERED" + }, + { + "action": "workspace.files.read", + "callsite": "LocalWorkspaceReadOwner.execute", + "owner": "workspace_read", + "status": "COVERED" + }, + { + "action": "workspace.config.read", + "callsite": "WorkspaceConfigOwner.read_config", + "owner": "workspace_config", + "status": "COVERED" + }, + { + "action": "workspace.config.list", + "callsite": "WorkspaceConfigOwner.commands", + "owner": "workspace_config", + "status": "COVERED" + }, + { + "action": "workspace.config.write", + "callsite": "WorkspaceConfigOwner.apply", + "owner": "workspace_config", + "status": "COVERED" + }, + { + "action": "workspace.context.inject", + "callsite": "WorkspaceMixin._inject_agent_context", + "owner": "workspace_read", + "status": "COVERED" + }, + { + "action": "workspace.files.read", + "callsite": "WorkspaceMixin._workspace_request", + "owner": "workspace_read", + "status": "COVERED" + }, + { + "action": "workspace.files.write", + "callsite": "WorkspaceWriteOwner.apply", + "owner": "workspace_write", + "status": "COVERED" + }, + { + "action": "workspace.files.restore", + "callsite": "WorkspaceWriteOwner._apply_undo", + "owner": "workspace_write", + "status": "COVERED" + }, + { + "action": "workspace.files.delete", + "callsite": "WorkspaceWriteOwner._apply_delete", + "owner": "workspace_write", + "status": "COVERED" + }, + { + "action": "workspace.files.move", + "callsite": "WorkspaceWriteOwner._apply_move", + "owner": "workspace_write", + "status": "COVERED" + }, + { + "action": "mcp.invoke", + "callsite": "GatewayMCPInvocationOwner.invoke", + "owner": "gateway_mcp", + "status": "COVERED" + }, + { + "action": "gateway.semantic.read", + "callsite": "GatewaySemanticOwner.invoke", + "owner": "gateway_semantic", + "status": "COVERED" + }, + { + "action": "lsp.start", + "callsite": "LPSSymbolOwner.search", + "owner": "lsp_symbols", + "status": "COVERED" + }, + { + "action": "lsp.diagnostics", + "callsite": "LPSSymbolOwner.diagnostics", + "owner": "lsp_symbols", + "status": "COVERED" + }, + { + "action": "grit.claims.read", + "callsite": "GritPeersOwner.claims", + "owner": "grit_peers", + "status": "COVERED" + }, + { + "action": "workspace.command.run", + "callsite": "CommandRunOwner.run", + "owner": "workspace_command", + "status": "COVERED" + }, + { + "action": "git.status", + "callsite": "GitOwner.status", + "owner": "workspace_git", + "status": "COVERED" + }, + { + "action": "git.status", + "callsite": "GitOwner.is_path_tracked", + "owner": "workspace_git", + "status": "COVERED" + }, + { + "action": "git.diff", + "callsite": "GitOwner.diff", + "owner": "workspace_git", + "status": "COVERED" + }, + { + "action": "git.commit", + "callsite": "GitOwner.commit", + "owner": "workspace_git", + "status": "COVERED" + }, + { + "action": "git.push", + "callsite": "PublishOwner.run", + "owner": "workspace_publish", + "status": "COVERED" + }, + { + "action": "mcp.local.start", + "callsite": "LocalMCPOwner.start", + "owner": "mcp_local", + "status": "COVERED" + }, + { + "action": "mcp.local.invoke", + "callsite": "LocalMCPOwner.call", + "owner": "mcp_local", + "status": "COVERED" + }, + { + "action": "broker.build", + "callsite": "BrokerProvisionOwner.provision", + "owner": "broker_provision", + "status": "COVERED" + }, + { + "action": "broker.start", + "callsite": "BrokerProvisionOwner.provision", + "owner": "broker_provision", + "status": "COVERED_VARIANT" + }, + { + "action": "broker.start", + "callsite": "BrokerManagementOwner.perform", + "owner": "broker_management", + "status": "COVERED_VARIANT" + }, + { + "action": "session.delete", + "callsite": "SessionDeleteOwner.delete", + "owner": "session_delete", + "status": "COVERED" + }, + { + "action": "workspace.memory.read", + "callsite": "WorkspaceMemoryOwner.execute", + "owner": "workspace_memory", + "status": "COVERED" + }, + { + "action": "workspace.memory.write", + "callsite": "WorkspaceMemoryOwner.execute", + "owner": "workspace_memory", + "status": "COVERED" + }, + { + "action": "workspace.memory.forget", + "callsite": "WorkspaceMemoryOwner.execute", + "owner": "workspace_memory", + "status": "COVERED" + }, + { + "action": "mobile.host.start", + "callsite": "MobileHostOwner.authorize_and_launch", + "owner": "mobile_host", + "status": "COVERED" + }, + { + "action": "mobile.host.stop", + "callsite": "MobileHostOwner.shutdown", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "mobile.pair", + "callsite": "MobileHostOwner.authorize_pair", + "owner": "mobile_host", + "status": "COVERED" + }, + { + "action": "mobile.pair.issue", + "callsite": "MobileHostOwner.issue_pairing_pin", + "owner": "mobile_host", + "status": "COVERED" + }, + { + "action": "mobile.pair", + "callsite": "ApiKeyStore.issue", + "owner": "mobile_host", + "status": "COVERED" + }, + { + "action": "mobile.pair", + "callsite": "ApiKeyStore.revoke", + "owner": "mobile_host", + "status": "COVERED" + }, + { + "action": "bridge.connect", + "callsite": "BridgeClient.hello", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "bridge.connect", + "callsite": "BridgeClient.heartbeat", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "bridge.connect", + "callsite": "BridgeClient.goodbye", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "bridge.connect", + "callsite": "BridgeClient.status", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "bridge.connect", + "callsite": "BridgeClient.agents", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "bridge.connect", + "callsite": "BridgeClient._run", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "bridge.peek", + "callsite": "BridgeClient.peek", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "bridge.lease.claim", + "callsite": "BridgeClient.claim", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "bridge.lease.release", + "callsite": "BridgeClient.release", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "bridge.send", + "callsite": "BridgeClient.send", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "credentials.add", + "callsite": "CredentialOwner.add", + "owner": "credentials", + "status": "COVERED" + }, + { + "action": "credentials.revoke", + "callsite": "CredentialOwner.revoke", + "owner": "credentials", + "status": "COVERED" + }, + { + "action": "credentials.use", + "callsite": "CredentialUseOwner.secret_for", + "owner": "credential_use", + "status": "COVERED" + }, + { + "action": "session.create", + "callsite": "ChatSessionOwner.record", + "owner": "chat_sessions", + "status": "COVERED" + }, + { + "action": "session.resume", + "callsite": "ChatSessionOwner.list_conversations", + "owner": "chat_sessions", + "status": "COVERED" + }, + { + "action": "session.resume", + "callsite": "ChatSessionOwner.resume", + "owner": "chat_sessions", + "status": "COVERED" + }, + { + "action": "session.create", + "callsite": "ChatSessionStore.create", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "session.create", + "callsite": "ChatSessionStore.rename", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "session.create", + "callsite": "ChatSessionStore.fork", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "desktop.file_picker", + "callsite": "RemoteMixin._open_broker_preview", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "desktop.file_picker", + "callsite": "RemoteMixin._provision_broker", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "desktop.file_picker", + "callsite": "tui_app_menu._readme_cmd", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "desktop.file_picker", + "callsite": "file_picker.choose_workspace_file", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "desktop.file_picker", + "callsite": "file_picker.choose_workspace_directory", + "owner": null, + "status": "BLOCKED_BY_DESIGN" + }, + { + "action": "clipboard.paste", + "callsite": "ClipboardOwner.paste", + "owner": "clipboard", + "status": "COVERED" + }, + { + "action": "clipboard.copy", + "callsite": "ClipboardOwner.copy", + "owner": "clipboard", + "status": "COVERED" + }, + { + "action": "bridge.agents", + "callsite": "BridgePresenceOwner.read", + "owner": "bridge_presence", + "status": "COVERED" + } + ], + "secure_tui_direct_api_bypasses": [], + "primitive_caller_violations": [], + "stale_callsites": [], + "request_constructors": [ + { + "file": "isycode/action_runtime.py", + "line": 92, + "callable": "isycode.action_runtime._workspace_config_request", + "action": null, + "owner": "workspace_config" + }, + { + "file": "isycode/action_runtime.py", + "line": 2172, + "callable": "isycode.action_runtime.LPSSymbolOwner.search", + "action": "workspace.files.read", + "owner": "lsp_symbols" + }, + { + "file": "isycode/action_runtime.py", + "line": 2298, + "callable": "isycode.action_runtime.GritPeersOwner.claims", + "action": "workspace.files.read", + "owner": "grit_peers" + }, + { + "file": "isycode/action_runtime.py", + "line": 1953, + "callable": "isycode.action_runtime.SessionDeleteOwner.delete", + "action": "session.delete", + "owner": "session_delete" + }, + { + "file": "isycode/action_runtime.py", + "line": 2025, + "callable": "isycode.action_runtime.GatewayMCPInvocationOwner.invoke", + "action": "mcp.invoke", + "owner": "gateway_mcp" + }, + { + "file": "isycode/action_runtime.py", + "line": 2095, + "callable": "isycode.action_runtime.GatewaySemanticOwner.invoke", + "action": "gateway.semantic.read", + "owner": "gateway_semantic" + }, + { + "file": "isycode/action_runtime.py", + "line": 2181, + "callable": "isycode.action_runtime.LPSSymbolOwner.search", + "action": "lsp.start", + "owner": "lsp_symbols" + }, + { + "file": "isycode/action_runtime.py", + "line": 2232, + "callable": "isycode.action_runtime.LPSSymbolOwner.diagnostics", + "action": "workspace.files.read", + "owner": "lsp_symbols" + }, + { + "file": "isycode/action_runtime.py", + "line": 2240, + "callable": "isycode.action_runtime.LPSSymbolOwner.diagnostics", + "action": "lsp.diagnostics", + "owner": "lsp_symbols" + }, + { + "file": "isycode/action_runtime.py", + "line": 2306, + "callable": "isycode.action_runtime.GritPeersOwner.claims", + "action": "grit.claims.read", + "owner": "grit_peers" + }, + { + "file": "isycode/action_runtime.py", + "line": 2368, + "callable": "isycode.action_runtime.ProviderNetworkOwner.execute", + "action": "provider.request", + "owner": "provider_network" + }, + { + "file": "isycode/action_runtime.py", + "line": 2449, + "callable": "isycode.action_runtime.LocalWorkspaceReadOwner._execute", + "action": null, + "owner": "workspace_read" + }, + { + "file": "isycode/bridge_presence.py", + "line": 110, + "callable": "isycode.bridge_presence.BridgePresenceOwner.prepare", + "action": "bridge.agents", + "owner": "bridge_presence" + }, + { + "file": "isycode/broker.py", + "line": 453, + "callable": "isycode.broker.provision_requests", + "action": "broker.build", + "owner": "broker_provision" + }, + { + "file": "isycode/broker.py", + "line": 458, + "callable": "isycode.broker.provision_requests", + "action": "broker.start", + "owner": "broker_provision" + }, + { + "file": "isycode/broker.py", + "line": 552, + "callable": "isycode.broker.BrokerProvisionOwner.provision", + "action": "workspace.files.read", + "owner": "broker_provision" + }, + { + "file": "isycode/broker.py", + "line": 485, + "callable": "isycode.broker.BrokerPreviewOwner.preview", + "action": "workspace.files.read", + "owner": "broker_preview" + }, + { + "file": "isycode/broker.py", + "line": 493, + "callable": "isycode.broker.BrokerPreviewOwner.preview", + "action": "broker.preview", + "owner": "broker_preview" + }, + { + "file": "isycode/broker.py", + "line": 749, + "callable": "isycode.broker.BrokerManagementOwner.request_for", + "action": null, + "owner": "broker_management" + }, + { + "file": "isycode/clipboard_owner.py", + "line": 48, + "callable": "isycode.clipboard_owner.ClipboardOwner.paste", + "action": "clipboard.paste", + "owner": "clipboard" + }, + { + "file": "isycode/clipboard_owner.py", + "line": 70, + "callable": "isycode.clipboard_owner.ClipboardOwner.copy", + "action": "clipboard.copy", + "owner": "clipboard" + }, + { + "file": "isycode/command_runner.py", + "line": 216, + "callable": "isycode.command_runner.CommandRunOwner.prepare", + "action": "workspace.command.run", + "owner": "workspace_command" + }, + { + "file": "isycode/credential_owner.py", + "line": 54, + "callable": "isycode.credential_owner.CredentialOwner.add_request", + "action": "credentials.add", + "owner": "credentials" + }, + { + "file": "isycode/credential_owner.py", + "line": 93, + "callable": "isycode.credential_owner.CredentialOwner.revoke_request", + "action": "credentials.revoke", + "owner": "credentials" + }, + { + "file": "isycode/credential_owner.py", + "line": 155, + "callable": "isycode.credential_owner.CredentialUseOwner.use_request", + "action": "credentials.use", + "owner": "credential_use" + }, + { + "file": "isycode/git_owner.py", + "line": 302, + "callable": "isycode.git_owner.GitOwner._request", + "action": null, + "owner": "workspace_git" + }, + { + "file": "isycode/grok_auth.py", + "line": 38, + "callable": "isycode.grok_auth.GrokAuthOwner.request", + "action": "grok.login", + "owner": "grok_auth" + }, + { + "file": "isycode/inspection_cli.py", + "line": 141, + "callable": "isycode.inspection_cli._sessions", + "action": "session.delete", + "owner": "session_delete" + }, + { + "file": "isycode/iteration.py", + "line": 198, + "callable": "isycode.iteration.IterationOwner.list_sessions", + "action": "session.resume", + "owner": "chat_sessions" + }, + { + "file": "isycode/iteration.py", + "line": 64, + "callable": "isycode.iteration.IterationOwner._authorize", + "action": "session.resume", + "owner": "chat_sessions" + }, + { + "file": "isycode/iteration.py", + "line": 68, + "callable": "isycode.iteration.IterationOwner._authorize", + "action": "session.create", + "owner": "chat_sessions" + }, + { + "file": "isycode/mcp_local.py", + "line": 241, + "callable": "isycode.mcp_local.LocalMCPOwner.prepare_start", + "action": "mcp.local.start", + "owner": "mcp_local" + }, + { + "file": "isycode/mcp_local.py", + "line": 343, + "callable": "isycode.mcp_local.LocalMCPOwner.prepare_call", + "action": "mcp.local.invoke", + "owner": "mcp_local" + }, + { + "file": "isycode/mobile_host.py", + "line": 482, + "callable": "isycode.mobile_host.MobileHostOwner.start_request", + "action": "mobile.host.start", + "owner": "mobile_host" + }, + { + "file": "isycode/mobile_host.py", + "line": 519, + "callable": "isycode.mobile_host.MobileHostOwner.pin_request", + "action": "mobile.pair.issue", + "owner": "mobile_host" + }, + { + "file": "isycode/mobile_host.py", + "line": 554, + "callable": "isycode.mobile_host.MobileHostOwner.authorize_pair", + "action": "mobile.pair", + "owner": "mobile_host" + }, + { + "file": "isycode/mobile_host.py", + "line": 562, + "callable": "isycode.mobile_host.MobileHostOwner.record_pair", + "action": "mobile.pair", + "owner": "mobile_host" + }, + { + "file": "isycode/provider_auth.py", + "line": 73, + "callable": "isycode.provider_auth.ProviderAuthOwner.request", + "action": "provider.authenticate", + "owner": "provider_auth" + }, + { + "file": "isycode/publish.py", + "line": 60, + "callable": "isycode.publish.PublishOwner.prepare", + "action": "git.push", + "owner": "workspace_publish" + }, + { + "file": "isycode/session_owner.py", + "line": 249, + "callable": "isycode.session_owner.ChatSessionOwner.list_conversations", + "action": "session.resume", + "owner": "chat_sessions" + }, + { + "file": "isycode/session_owner.py", + "line": 336, + "callable": "isycode.session_owner.ChatSessionOwner.manage", + "action": "session.create", + "owner": "chat_sessions" + }, + { + "file": "isycode/session_owner.py", + "line": 128, + "callable": "isycode.session_owner.ChatSessionOwner._note", + "action": "session.resume", + "owner": "chat_sessions" + }, + { + "file": "isycode/session_owner.py", + "line": 206, + "callable": "isycode.session_owner.ChatSessionOwner.record", + "action": "session.create", + "owner": "chat_sessions" + }, + { + "file": "isycode/session_owner.py", + "line": 278, + "callable": "isycode.session_owner.ChatSessionOwner._load", + "action": "session.resume", + "owner": "chat_sessions" + }, + { + "file": "isycode/tailscale_install.py", + "line": 117, + "callable": "isycode.tailscale_install.UbuntuDebianInstallPlan.prepare_request", + "action": "tailscale.install.prepare", + "owner": "tailscale_package_install" + }, + { + "file": "isycode/tailscale_install.py", + "line": 220, + "callable": "isycode.tailscale_install.PreparedTransaction.stage_request", + "action": "tailscale.install.stage", + "owner": "tailscale_package_install" + }, + { + "file": "isycode/tailscale_install.py", + "line": 224, + "callable": "isycode.tailscale_install.PreparedTransaction.request", + "action": "tailscale.install", + "owner": "tailscale_package_install" + }, + { + "file": "isycode/tailscale_login.py", + "line": 130, + "callable": "isycode.tailscale_login.TailscaleLoginOwner.login_request", + "action": "tailscale.login", + "owner": "tailscale_login" + }, + { + "file": "isycode/tailscale_login.py", + "line": 167, + "callable": "isycode.tailscale_login.TailscaleLoginOwner.begin_login", + "action": "tailscale.login", + "owner": "tailscale_login" + }, + { + "file": "isycode/tailscale_read.py", + "line": 39, + "callable": "isycode.tailscale_read.TailscaleReadOwner.inspect", + "action": "tailscale.inspect", + "owner": "tailscale_read" + }, + { + "file": "isycode/tailscale_serve.py", + "line": 189, + "callable": "isycode.tailscale_serve.TailscaleServeOwner._prepare", + "action": null, + "owner": "tailscale_serve" + }, + { + "file": "isycode/tui_app_authority.py", + "line": 835, + "callable": "isycode.tui_app_authority.AuthorityMixin._authorize_remote_read", + "action": null, + "owner": "remote_catalog" + }, + { + "file": "isycode/tui_app_remote.py", + "line": 632, + "callable": "isycode.tui_app_remote.RemoteMixin._open_gateway_mcp_tool", + "action": "mcp.invoke", + "owner": "gateway_mcp" + }, + { + "file": "isycode/tui_app_remote.py", + "line": 684, + "callable": "isycode.tui_app_remote.RemoteMixin._open_gateway_semantic_search", + "action": "gateway.semantic.read", + "owner": "gateway_semantic" + }, + { + "file": "isycode/tui_app_remote.py", + "line": 815, + "callable": "isycode.tui_app_remote.RemoteMixin._open_lsp_server", + "action": "lsp.start", + "owner": "lsp_symbols" + }, + { + "file": "isycode/tui_app_sessions.py", + "line": 977, + "callable": "isycode.tui_app_sessions.SessionMixin._delete_chat_session", + "action": "session.delete", + "owner": "session_delete" + }, + { + "file": "isycode/tui_app_sessions.py", + "line": 1017, + "callable": "isycode.tui_app_sessions.SessionMixin._delete_iteration_session", + "action": "session.delete", + "owner": "session_delete" + }, + { + "file": "isycode/tui_app_workspace.py", + "line": 106, + "callable": "isycode.tui_app_workspace.WorkspaceMixin._confirm_and_load_external_context", + "action": "workspace.context.inject", + "owner": "workspace_read" + }, + { + "file": "isycode/tui_app_workspace.py", + "line": 190, + "callable": "isycode.tui_app_workspace.WorkspaceMixin._workspace_request", + "action": null, + "owner": "workspace_read" + }, + { + "file": "isycode/web_fetch.py", + "line": 184, + "callable": "isycode.web_fetch.WebFetchOwner.execute", + "action": "web.fetch", + "owner": "web_fetch" + }, + { + "file": "isycode/workspace_memory.py", + "line": 547, + "callable": "isycode.workspace_memory.WorkspaceMemoryOwner.prepare", + "action": null, + "owner": "workspace_memory" + }, + { + "file": "isycode/workspace_write.py", + "line": 461, + "callable": "isycode.workspace_write.WorkspaceWriteOwner.preview_undo", + "action": "workspace.files.restore", + "owner": "workspace_write" + }, + { + "file": "isycode/workspace_write.py", + "line": 478, + "callable": "isycode.workspace_write.WorkspaceWriteOwner.preview_delete", + "action": "workspace.files.delete", + "owner": "workspace_write" + }, + { + "file": "isycode/workspace_write.py", + "line": 501, + "callable": "isycode.workspace_write.WorkspaceWriteOwner._preview_move", + "action": "workspace.files.move", + "owner": "workspace_write" + }, + { + "file": "isycode/workspace_write.py", + "line": 412, + "callable": "isycode.workspace_write.WorkspaceWriteOwner.preview", + "action": "workspace.config.write", + "owner": "workspace_config" + }, + { + "file": "isycode/workspace_write.py", + "line": 415, + "callable": "isycode.workspace_write.WorkspaceWriteOwner.preview", + "action": "workspace.files.write", + "owner": "workspace_write" } - ], - "status": "OWNER_VARIANTS", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "broker.health", - "effect": "read", - "approval_required": false, - "owners": [ - "broker_management" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "BrokerRegistryBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "NOT_DEMONSTRATED" - }, - { - "action": "broker.logs", - "effect": "read", - "approval_required": true, - "owners": [ - "broker_management" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "BrokerRegistryBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "NOT_DEMONSTRATED" - }, - { - "action": "broker.stop", - "effect": "process", - "approval_required": true, - "owners": [ - "broker_management" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "BrokerRegistryBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "NOT_DEMONSTRATED" - }, - { - "action": "broker.remove", - "effect": "destructive", - "approval_required": true, - "owners": [ - "broker_management" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "BrokerRegistryBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "NOT_DEMONSTRATED" - }, - { - "action": "mobile.host.start", - "effect": "network", - "approval_required": true, - "owners": [ - "mobile_host" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "MobileHostBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "mobile.host.stop", - "effect": "process", - "approval_required": false, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "mobile.pair", - "effect": "credential", - "approval_required": false, - "owners": [ - "mobile_host" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "MobileHostBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "mobile.pair.issue", - "effect": "credential", - "approval_required": true, - "owners": [ - "mobile_host" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "MobileHostBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "mobile.session.read", - "effect": "read", - "approval_required": false, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "mobile.session.create", - "effect": "external", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "mobile.session.cancel", - "effect": "external", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "mobile.approval.respond", - "effect": "external", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "bridge.connect", - "effect": "network", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "bridge.peek", - "effect": "read", - "approval_required": false, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "bridge.send", - "effect": "external", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "bridge.lease.claim", - "effect": "coordination", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "bridge.lease.release", - "effect": "coordination", - "approval_required": false, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "bridge.wake", - "effect": "external", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "bridge.agents", - "effect": "read", - "approval_required": true, - "owners": [ - "bridge_presence" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "BridgePresenceBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "l1.create", - "effect": "write", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "l1.validate", - "effect": "process", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "l1.test", - "effect": "process", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "l1.activate", - "effect": "write", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "l1.disable", - "effect": "write", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "l1.rollback", - "effect": "write", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "session.create", - "effect": "write", - "approval_required": false, - "owners": [ - "chat_sessions" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "SessionStoreBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "session.resume", - "effect": "read", - "approval_required": false, - "owners": [ - "chat_sessions" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "SessionStoreBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "session.delete", - "effect": "destructive", - "approval_required": true, - "owners": [ - "session_delete" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "SessionDeleteBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.memory.read", - "effect": "read", - "approval_required": false, - "owners": [ - "workspace_memory" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceMemoryBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.memory.write", - "effect": "write", - "approval_required": true, - "owners": [ - "workspace_memory" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceMemoryBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "workspace.memory.forget", - "effect": "destructive", - "approval_required": true, - "owners": [ - "workspace_memory" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "WorkspaceMemoryBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "clipboard.paste", - "effect": "external", - "approval_required": false, - "owners": [ - "clipboard" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "ClipboardBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "clipboard.copy", - "effect": "external", - "approval_required": false, - "owners": [ - "clipboard" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "ClipboardBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "desktop.file_picker", - "effect": "process", - "approval_required": true, - "owners": [], - "classification": "EXPLICIT_DENY", - "authority": "explicit deny at owner binding", - "sentinel": [], - "variants": [], - "status": "EXPLICIT_DENY", - "secure_reachability": "SECURE_EXCLUDED_BY_OWNER_BINDING" - }, - { - "action": "tailscale.inspect", - "effect": "read", - "approval_required": false, - "owners": [ - "tailscale_read" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "TailscaleExecutableBoundary", - "TailscaleGatewayBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "tailscale.install.prepare", - "effect": "process", - "approval_required": true, - "owners": [ - "tailscale_package_install" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "TailscalePackageBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "tailscale.install.stage", - "effect": "process", - "approval_required": true, - "owners": [ - "tailscale_package_install" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "TailscalePackageBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "tailscale.install", - "effect": "process", - "approval_required": true, - "owners": [ - "tailscale_package_install" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "TailscalePackageBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "tailscale.login", - "effect": "process", - "approval_required": true, - "owners": [ - "tailscale_login" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "TailscaleExecutableBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "tailscale.serve.enable", - "effect": "process", - "approval_required": true, - "owners": [ - "tailscale_serve" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "TailscaleExecutableBoundary", - "TailscaleGatewayBoundary", - "TailscalePrivateServeBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "tailscale.serve.disable", - "effect": "process", - "approval_required": true, - "owners": [ - "tailscale_serve" - ], - "classification": "OWNER_VALID", - "authority": "Workspace Authority", - "sentinel": [ - "TailscaleExecutableBoundary", - "TailscaleGatewayBoundary", - "TailscalePrivateServeBoundary" - ], - "variants": [], - "status": "OWNER_VALID", - "secure_reachability": "OWNER_PATH_DECLARED; END_TO_END_REACHABILITY_NOT_PROVEN" - }, - { - "action": "role.select", - "effect": "read", - "approval_required": false, - "owners": [], - "classification": "SECURE_EXCLUDED", - "authority": "not an effect authority action", - "sentinel": [], - "variants": [], - "status": "SECURE_EXCLUDED", - "secure_reachability": "NOT_AN_EFFECT_ACTION" - } - ], - "owner_action_mismatches": [], - "owner_contract_gaps": [], - "variant_contract_gaps": [], - "ambiguous_actions": [], - "unclassified_actions": [], - "callsites": [ - { - "action": "web.fetch", - "callsite": "WebFetchOwner.execute", - "owner": "web_fetch", - "status": "COVERED" - }, - { - "action": "session.create", - "callsite": "IterationOwner._persist", - "owner": "chat_sessions", - "status": "COVERED" - }, - { - "action": "session.resume", - "callsite": "IterationOwner.inspect", - "owner": "chat_sessions", - "status": "COVERED" - }, - { - "action": "provider.request", - "callsite": "run_iteration.complete", - "owner": "provider_network", - "status": "COVERED" - }, - { - "action": "tailscale.inspect", - "callsite": "TailscaleReadOwner.inspect", - "owner": "tailscale_read", - "status": "COVERED" - }, - { - "action": "tailscale.install.prepare", - "callsite": "TailscalePackageInstallOwner.prepare", - "owner": "tailscale_package_install", - "status": "COVERED" - }, - { - "action": "tailscale.install.stage", - "callsite": "TailscalePackageInstallOwner.stage", - "owner": "tailscale_package_install", - "status": "COVERED" - }, - { - "action": "tailscale.install", - "callsite": "TailscalePackageInstallOwner.install", - "owner": "tailscale_package_install", - "status": "COVERED" - }, - { - "action": "tailscale.login", - "callsite": "TailscaleLoginOwner.begin_login", - "owner": "tailscale_login", - "status": "COVERED" - }, - { - "action": "tailscale.serve.enable", - "callsite": "TailscaleServeOwner.enable", - "owner": "tailscale_serve", - "status": "COVERED" - }, - { - "action": "tailscale.serve.disable", - "callsite": "TailscaleServeOwner.disable", - "owner": "tailscale_serve", - "status": "COVERED" - }, - { - "action": "provider.request", - "callsite": "ProviderNetworkOwner.execute", - "owner": "provider_network", - "status": "COVERED" - }, - { - "action": "provider.authenticate", - "callsite": "ProviderAuthOwner.begin", - "owner": "provider_auth", - "status": "COVERED" - }, - { - "action": "grok.login", - "callsite": "GrokAuthOwner.begin", - "owner": "grok_auth", - "status": "COVERED" - }, - { - "action": "workspace.files.read", - "callsite": "LocalWorkspaceReadOwner.execute", - "owner": "workspace_read", - "status": "COVERED" - }, - { - "action": "workspace.config.read", - "callsite": "WorkspaceConfigOwner.read_config", - "owner": "workspace_config", - "status": "COVERED" - }, - { - "action": "workspace.config.list", - "callsite": "WorkspaceConfigOwner.commands", - "owner": "workspace_config", - "status": "COVERED" - }, - { - "action": "workspace.config.write", - "callsite": "WorkspaceConfigOwner.apply", - "owner": "workspace_config", - "status": "COVERED" - }, - { - "action": "workspace.context.inject", - "callsite": "WorkspaceMixin._inject_agent_context", - "owner": "workspace_read", - "status": "COVERED" - }, - { - "action": "workspace.files.read", - "callsite": "WorkspaceMixin._workspace_request", - "owner": "workspace_read", - "status": "COVERED" - }, - { - "action": "workspace.files.write", - "callsite": "WorkspaceWriteOwner.apply", - "owner": "workspace_write", - "status": "COVERED" - }, - { - "action": "workspace.files.restore", - "callsite": "WorkspaceWriteOwner._apply_undo", - "owner": "workspace_write", - "status": "COVERED" - }, - { - "action": "workspace.files.delete", - "callsite": "WorkspaceWriteOwner._apply_delete", - "owner": "workspace_write", - "status": "COVERED" - }, - { - "action": "workspace.files.move", - "callsite": "WorkspaceWriteOwner._apply_move", - "owner": "workspace_write", - "status": "COVERED" - }, - { - "action": "mcp.invoke", - "callsite": "GatewayMCPInvocationOwner.invoke", - "owner": "gateway_mcp", - "status": "COVERED" - }, - { - "action": "gateway.semantic.read", - "callsite": "GatewaySemanticOwner.invoke", - "owner": "gateway_semantic", - "status": "COVERED" - }, - { - "action": "lsp.start", - "callsite": "LPSSymbolOwner.search", - "owner": "lsp_symbols", - "status": "COVERED" - }, - { - "action": "lsp.diagnostics", - "callsite": "LPSSymbolOwner.diagnostics", - "owner": "lsp_symbols", - "status": "COVERED" - }, - { - "action": "grit.claims.read", - "callsite": "GritPeersOwner.claims", - "owner": "grit_peers", - "status": "COVERED" - }, - { - "action": "workspace.command.run", - "callsite": "CommandRunOwner.run", - "owner": "workspace_command", - "status": "COVERED" - }, - { - "action": "git.status", - "callsite": "GitOwner.status", - "owner": "workspace_git", - "status": "COVERED" - }, - { - "action": "git.status", - "callsite": "GitOwner.is_path_tracked", - "owner": "workspace_git", - "status": "COVERED" - }, - { - "action": "git.diff", - "callsite": "GitOwner.diff", - "owner": "workspace_git", - "status": "COVERED" - }, - { - "action": "git.commit", - "callsite": "GitOwner.commit", - "owner": "workspace_git", - "status": "COVERED" - }, - { - "action": "git.push", - "callsite": "PublishOwner.run", - "owner": "workspace_publish", - "status": "COVERED" - }, - { - "action": "mcp.local.start", - "callsite": "LocalMCPOwner.start", - "owner": "mcp_local", - "status": "COVERED" - }, - { - "action": "mcp.local.invoke", - "callsite": "LocalMCPOwner.call", - "owner": "mcp_local", - "status": "COVERED" - }, - { - "action": "broker.build", - "callsite": "BrokerProvisionOwner.provision", - "owner": "broker_provision", - "status": "COVERED" - }, - { - "action": "broker.start", - "callsite": "BrokerProvisionOwner.provision", - "owner": "broker_provision", - "status": "COVERED_VARIANT" - }, - { - "action": "broker.start", - "callsite": "BrokerManagementOwner.perform", - "owner": "broker_management", - "status": "COVERED_VARIANT" - }, - { - "action": "session.delete", - "callsite": "SessionDeleteOwner.delete", - "owner": "session_delete", - "status": "COVERED" - }, - { - "action": "workspace.memory.read", - "callsite": "WorkspaceMemoryOwner.execute", - "owner": "workspace_memory", - "status": "COVERED" - }, - { - "action": "workspace.memory.write", - "callsite": "WorkspaceMemoryOwner.execute", - "owner": "workspace_memory", - "status": "COVERED" - }, - { - "action": "workspace.memory.forget", - "callsite": "WorkspaceMemoryOwner.execute", - "owner": "workspace_memory", - "status": "COVERED" - }, - { - "action": "mobile.host.start", - "callsite": "MobileHostOwner.authorize_and_launch", - "owner": "mobile_host", - "status": "COVERED" - }, - { - "action": "mobile.host.stop", - "callsite": "MobileHostOwner.shutdown", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "mobile.pair", - "callsite": "MobileHostOwner.authorize_pair", - "owner": "mobile_host", - "status": "COVERED" - }, - { - "action": "mobile.pair.issue", - "callsite": "MobileHostOwner.issue_pairing_pin", - "owner": "mobile_host", - "status": "COVERED" - }, - { - "action": "mobile.pair", - "callsite": "ApiKeyStore.issue", - "owner": "mobile_host", - "status": "COVERED" - }, - { - "action": "mobile.pair", - "callsite": "ApiKeyStore.revoke", - "owner": "mobile_host", - "status": "COVERED" - }, - { - "action": "bridge.connect", - "callsite": "BridgeClient.hello", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "bridge.connect", - "callsite": "BridgeClient.heartbeat", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "bridge.connect", - "callsite": "BridgeClient.goodbye", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "bridge.connect", - "callsite": "BridgeClient.status", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "bridge.connect", - "callsite": "BridgeClient.agents", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "bridge.connect", - "callsite": "BridgeClient._run", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "bridge.peek", - "callsite": "BridgeClient.peek", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "bridge.lease.claim", - "callsite": "BridgeClient.claim", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "bridge.lease.release", - "callsite": "BridgeClient.release", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "bridge.send", - "callsite": "BridgeClient.send", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "credentials.add", - "callsite": "CredentialOwner.add", - "owner": "credentials", - "status": "COVERED" - }, - { - "action": "credentials.revoke", - "callsite": "CredentialOwner.revoke", - "owner": "credentials", - "status": "COVERED" - }, - { - "action": "credentials.use", - "callsite": "CredentialUseOwner.secret_for", - "owner": "credential_use", - "status": "COVERED" - }, - { - "action": "session.create", - "callsite": "ChatSessionOwner.record", - "owner": "chat_sessions", - "status": "COVERED" - }, - { - "action": "session.resume", - "callsite": "ChatSessionOwner.list_conversations", - "owner": "chat_sessions", - "status": "COVERED" - }, - { - "action": "session.resume", - "callsite": "ChatSessionOwner.resume", - "owner": "chat_sessions", - "status": "COVERED" - }, - { - "action": "session.create", - "callsite": "ChatSessionStore.create", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "session.create", - "callsite": "ChatSessionStore.rename", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "session.create", - "callsite": "ChatSessionStore.fork", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "desktop.file_picker", - "callsite": "RemoteMixin._open_broker_preview", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "desktop.file_picker", - "callsite": "RemoteMixin._provision_broker", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "desktop.file_picker", - "callsite": "tui_app_menu._readme_cmd", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "desktop.file_picker", - "callsite": "file_picker.choose_workspace_file", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "desktop.file_picker", - "callsite": "file_picker.choose_workspace_directory", - "owner": null, - "status": "BLOCKED_BY_DESIGN" - }, - { - "action": "clipboard.paste", - "callsite": "ClipboardOwner.paste", - "owner": "clipboard", - "status": "COVERED" - }, - { - "action": "clipboard.copy", - "callsite": "ClipboardOwner.copy", - "owner": "clipboard", - "status": "COVERED" - }, - { - "action": "bridge.agents", - "callsite": "BridgePresenceOwner.read", - "owner": "bridge_presence", - "status": "COVERED" - } - ], - "secure_tui_direct_api_bypasses": [], - "primitive_caller_violations": [], - "stale_callsites": [], - "request_constructors": [ - { - "file": "isycode/action_runtime.py", - "line": 92, - "callable": "isycode.action_runtime._workspace_config_request", - "action": null, - "owner": "workspace_config" - }, - { - "file": "isycode/action_runtime.py", - "line": 2172, - "callable": "isycode.action_runtime.LPSSymbolOwner.search", - "action": "workspace.files.read", - "owner": "lsp_symbols" - }, - { - "file": "isycode/action_runtime.py", - "line": 2298, - "callable": "isycode.action_runtime.GritPeersOwner.claims", - "action": "workspace.files.read", - "owner": "grit_peers" - }, - { - "file": "isycode/action_runtime.py", - "line": 1953, - "callable": "isycode.action_runtime.SessionDeleteOwner.delete", - "action": "session.delete", - "owner": "session_delete" - }, - { - "file": "isycode/action_runtime.py", - "line": 2025, - "callable": "isycode.action_runtime.GatewayMCPInvocationOwner.invoke", - "action": "mcp.invoke", - "owner": "gateway_mcp" - }, - { - "file": "isycode/action_runtime.py", - "line": 2095, - "callable": "isycode.action_runtime.GatewaySemanticOwner.invoke", - "action": "gateway.semantic.read", - "owner": "gateway_semantic" - }, - { - "file": "isycode/action_runtime.py", - "line": 2181, - "callable": "isycode.action_runtime.LPSSymbolOwner.search", - "action": "lsp.start", - "owner": "lsp_symbols" - }, - { - "file": "isycode/action_runtime.py", - "line": 2232, - "callable": "isycode.action_runtime.LPSSymbolOwner.diagnostics", - "action": "workspace.files.read", - "owner": "lsp_symbols" - }, - { - "file": "isycode/action_runtime.py", - "line": 2240, - "callable": "isycode.action_runtime.LPSSymbolOwner.diagnostics", - "action": "lsp.diagnostics", - "owner": "lsp_symbols" - }, - { - "file": "isycode/action_runtime.py", - "line": 2306, - "callable": "isycode.action_runtime.GritPeersOwner.claims", - "action": "grit.claims.read", - "owner": "grit_peers" - }, - { - "file": "isycode/action_runtime.py", - "line": 2368, - "callable": "isycode.action_runtime.ProviderNetworkOwner.execute", - "action": "provider.request", - "owner": "provider_network" - }, - { - "file": "isycode/action_runtime.py", - "line": 2449, - "callable": "isycode.action_runtime.LocalWorkspaceReadOwner._execute", - "action": null, - "owner": "workspace_read" - }, - { - "file": "isycode/bridge_presence.py", - "line": 110, - "callable": "isycode.bridge_presence.BridgePresenceOwner.prepare", - "action": "bridge.agents", - "owner": "bridge_presence" - }, - { - "file": "isycode/broker.py", - "line": 453, - "callable": "isycode.broker.provision_requests", - "action": "broker.build", - "owner": "broker_provision" - }, - { - "file": "isycode/broker.py", - "line": 458, - "callable": "isycode.broker.provision_requests", - "action": "broker.start", - "owner": "broker_provision" - }, - { - "file": "isycode/broker.py", - "line": 552, - "callable": "isycode.broker.BrokerProvisionOwner.provision", - "action": "workspace.files.read", - "owner": "broker_provision" - }, - { - "file": "isycode/broker.py", - "line": 485, - "callable": "isycode.broker.BrokerPreviewOwner.preview", - "action": "workspace.files.read", - "owner": "broker_preview" - }, - { - "file": "isycode/broker.py", - "line": 493, - "callable": "isycode.broker.BrokerPreviewOwner.preview", - "action": "broker.preview", - "owner": "broker_preview" - }, - { - "file": "isycode/broker.py", - "line": 749, - "callable": "isycode.broker.BrokerManagementOwner.request_for", - "action": null, - "owner": "broker_management" - }, - { - "file": "isycode/clipboard_owner.py", - "line": 48, - "callable": "isycode.clipboard_owner.ClipboardOwner.paste", - "action": "clipboard.paste", - "owner": "clipboard" - }, - { - "file": "isycode/clipboard_owner.py", - "line": 70, - "callable": "isycode.clipboard_owner.ClipboardOwner.copy", - "action": "clipboard.copy", - "owner": "clipboard" - }, - { - "file": "isycode/command_runner.py", - "line": 216, - "callable": "isycode.command_runner.CommandRunOwner.prepare", - "action": "workspace.command.run", - "owner": "workspace_command" - }, - { - "file": "isycode/credential_owner.py", - "line": 54, - "callable": "isycode.credential_owner.CredentialOwner.add_request", - "action": "credentials.add", - "owner": "credentials" - }, - { - "file": "isycode/credential_owner.py", - "line": 93, - "callable": "isycode.credential_owner.CredentialOwner.revoke_request", - "action": "credentials.revoke", - "owner": "credentials" - }, - { - "file": "isycode/credential_owner.py", - "line": 155, - "callable": "isycode.credential_owner.CredentialUseOwner.use_request", - "action": "credentials.use", - "owner": "credential_use" - }, - { - "file": "isycode/git_owner.py", - "line": 302, - "callable": "isycode.git_owner.GitOwner._request", - "action": null, - "owner": "workspace_git" - }, - { - "file": "isycode/grok_auth.py", - "line": 38, - "callable": "isycode.grok_auth.GrokAuthOwner.request", - "action": "grok.login", - "owner": "grok_auth" - }, - { - "file": "isycode/inspection_cli.py", - "line": 141, - "callable": "isycode.inspection_cli._sessions", - "action": "session.delete", - "owner": "session_delete" - }, - { - "file": "isycode/iteration.py", - "line": 198, - "callable": "isycode.iteration.IterationOwner.list_sessions", - "action": "session.resume", - "owner": "chat_sessions" - }, - { - "file": "isycode/iteration.py", - "line": 64, - "callable": "isycode.iteration.IterationOwner._authorize", - "action": "session.resume", - "owner": "chat_sessions" - }, - { - "file": "isycode/iteration.py", - "line": 68, - "callable": "isycode.iteration.IterationOwner._authorize", - "action": "session.create", - "owner": "chat_sessions" - }, - { - "file": "isycode/mcp_local.py", - "line": 241, - "callable": "isycode.mcp_local.LocalMCPOwner.prepare_start", - "action": "mcp.local.start", - "owner": "mcp_local" - }, - { - "file": "isycode/mcp_local.py", - "line": 343, - "callable": "isycode.mcp_local.LocalMCPOwner.prepare_call", - "action": "mcp.local.invoke", - "owner": "mcp_local" - }, - { - "file": "isycode/mobile_host.py", - "line": 482, - "callable": "isycode.mobile_host.MobileHostOwner.start_request", - "action": "mobile.host.start", - "owner": "mobile_host" - }, - { - "file": "isycode/mobile_host.py", - "line": 519, - "callable": "isycode.mobile_host.MobileHostOwner.pin_request", - "action": "mobile.pair.issue", - "owner": "mobile_host" - }, - { - "file": "isycode/mobile_host.py", - "line": 554, - "callable": "isycode.mobile_host.MobileHostOwner.authorize_pair", - "action": "mobile.pair", - "owner": "mobile_host" - }, - { - "file": "isycode/mobile_host.py", - "line": 562, - "callable": "isycode.mobile_host.MobileHostOwner.record_pair", - "action": "mobile.pair", - "owner": "mobile_host" - }, - { - "file": "isycode/provider_auth.py", - "line": 73, - "callable": "isycode.provider_auth.ProviderAuthOwner.request", - "action": "provider.authenticate", - "owner": "provider_auth" - }, - { - "file": "isycode/publish.py", - "line": 60, - "callable": "isycode.publish.PublishOwner.prepare", - "action": "git.push", - "owner": "workspace_publish" - }, - { - "file": "isycode/session_owner.py", - "line": 249, - "callable": "isycode.session_owner.ChatSessionOwner.list_conversations", - "action": "session.resume", - "owner": "chat_sessions" - }, - { - "file": "isycode/session_owner.py", - "line": 336, - "callable": "isycode.session_owner.ChatSessionOwner.manage", - "action": "session.create", - "owner": "chat_sessions" - }, - { - "file": "isycode/session_owner.py", - "line": 128, - "callable": "isycode.session_owner.ChatSessionOwner._note", - "action": "session.resume", - "owner": "chat_sessions" - }, - { - "file": "isycode/session_owner.py", - "line": 206, - "callable": "isycode.session_owner.ChatSessionOwner.record", - "action": "session.create", - "owner": "chat_sessions" - }, - { - "file": "isycode/session_owner.py", - "line": 278, - "callable": "isycode.session_owner.ChatSessionOwner._load", - "action": "session.resume", - "owner": "chat_sessions" - }, - { - "file": "isycode/tailscale_install.py", - "line": 117, - "callable": "isycode.tailscale_install.UbuntuDebianInstallPlan.prepare_request", - "action": "tailscale.install.prepare", - "owner": "tailscale_package_install" - }, - { - "file": "isycode/tailscale_install.py", - "line": 220, - "callable": "isycode.tailscale_install.PreparedTransaction.stage_request", - "action": "tailscale.install.stage", - "owner": "tailscale_package_install" - }, - { - "file": "isycode/tailscale_install.py", - "line": 224, - "callable": "isycode.tailscale_install.PreparedTransaction.request", - "action": "tailscale.install", - "owner": "tailscale_package_install" - }, - { - "file": "isycode/tailscale_login.py", - "line": 130, - "callable": "isycode.tailscale_login.TailscaleLoginOwner.login_request", - "action": "tailscale.login", - "owner": "tailscale_login" - }, - { - "file": "isycode/tailscale_login.py", - "line": 167, - "callable": "isycode.tailscale_login.TailscaleLoginOwner.begin_login", - "action": "tailscale.login", - "owner": "tailscale_login" - }, - { - "file": "isycode/tailscale_read.py", - "line": 39, - "callable": "isycode.tailscale_read.TailscaleReadOwner.inspect", - "action": "tailscale.inspect", - "owner": "tailscale_read" - }, - { - "file": "isycode/tailscale_serve.py", - "line": 189, - "callable": "isycode.tailscale_serve.TailscaleServeOwner._prepare", - "action": null, - "owner": "tailscale_serve" - }, - { - "file": "isycode/tui_app_authority.py", - "line": 835, - "callable": "isycode.tui_app_authority.AuthorityMixin._authorize_remote_read", - "action": null, - "owner": "remote_catalog" - }, - { - "file": "isycode/tui_app_remote.py", - "line": 632, - "callable": "isycode.tui_app_remote.RemoteMixin._open_gateway_mcp_tool", - "action": "mcp.invoke", - "owner": "gateway_mcp" - }, - { - "file": "isycode/tui_app_remote.py", - "line": 684, - "callable": "isycode.tui_app_remote.RemoteMixin._open_gateway_semantic_search", - "action": "gateway.semantic.read", - "owner": "gateway_semantic" - }, - { - "file": "isycode/tui_app_remote.py", - "line": 815, - "callable": "isycode.tui_app_remote.RemoteMixin._open_lsp_server", - "action": "lsp.start", - "owner": "lsp_symbols" - }, - { - "file": "isycode/tui_app_sessions.py", - "line": 963, - "callable": "isycode.tui_app_sessions.SessionMixin._delete_chat_session", - "action": "session.delete", - "owner": "session_delete" - }, - { - "file": "isycode/tui_app_sessions.py", - "line": 1003, - "callable": "isycode.tui_app_sessions.SessionMixin._delete_iteration_session", - "action": "session.delete", - "owner": "session_delete" - }, - { - "file": "isycode/tui_app_workspace.py", - "line": 106, - "callable": "isycode.tui_app_workspace.WorkspaceMixin._confirm_and_load_external_context", - "action": "workspace.context.inject", - "owner": "workspace_read" - }, - { - "file": "isycode/tui_app_workspace.py", - "line": 190, - "callable": "isycode.tui_app_workspace.WorkspaceMixin._workspace_request", - "action": null, - "owner": "workspace_read" - }, - { - "file": "isycode/web_fetch.py", - "line": 184, - "callable": "isycode.web_fetch.WebFetchOwner.execute", - "action": "web.fetch", - "owner": "web_fetch" - }, - { - "file": "isycode/workspace_memory.py", - "line": 547, - "callable": "isycode.workspace_memory.WorkspaceMemoryOwner.prepare", - "action": null, - "owner": "workspace_memory" - }, - { - "file": "isycode/workspace_write.py", - "line": 461, - "callable": "isycode.workspace_write.WorkspaceWriteOwner.preview_undo", - "action": "workspace.files.restore", - "owner": "workspace_write" - }, - { - "file": "isycode/workspace_write.py", - "line": 478, - "callable": "isycode.workspace_write.WorkspaceWriteOwner.preview_delete", - "action": "workspace.files.delete", - "owner": "workspace_write" - }, - { - "file": "isycode/workspace_write.py", - "line": 501, - "callable": "isycode.workspace_write.WorkspaceWriteOwner._preview_move", - "action": "workspace.files.move", - "owner": "workspace_write" - }, - { - "file": "isycode/workspace_write.py", - "line": 412, - "callable": "isycode.workspace_write.WorkspaceWriteOwner.preview", - "action": "workspace.config.write", - "owner": "workspace_config" - }, - { - "file": "isycode/workspace_write.py", - "line": 415, - "callable": "isycode.workspace_write.WorkspaceWriteOwner.preview", - "action": "workspace.files.write", - "owner": "workspace_write" - } - ], - "dynamic_request_constructors": [ - { - "file": "isycode/action_runtime.py", - "line": 92, - "callable": "isycode.action_runtime._workspace_config_request", - "action": null, - "owner": "workspace_config" - }, - { - "file": "isycode/action_runtime.py", - "line": 2449, - "callable": "isycode.action_runtime.LocalWorkspaceReadOwner._execute", - "action": null, - "owner": "workspace_read" - }, - { - "file": "isycode/broker.py", - "line": 749, - "callable": "isycode.broker.BrokerManagementOwner.request_for", - "action": null, - "owner": "broker_management" - }, - { - "file": "isycode/git_owner.py", - "line": 302, - "callable": "isycode.git_owner.GitOwner._request", - "action": null, - "owner": "workspace_git" - }, - { - "file": "isycode/tailscale_serve.py", - "line": 189, - "callable": "isycode.tailscale_serve.TailscaleServeOwner._prepare", - "action": null, - "owner": "tailscale_serve" - }, - { - "file": "isycode/tui_app_authority.py", - "line": 835, - "callable": "isycode.tui_app_authority.AuthorityMixin._authorize_remote_read", - "action": null, - "owner": "remote_catalog" - }, - { - "file": "isycode/tui_app_workspace.py", - "line": 190, - "callable": "isycode.tui_app_workspace.WorkspaceMixin._workspace_request", - "action": null, - "owner": "workspace_read" - }, - { - "file": "isycode/workspace_memory.py", - "line": 547, - "callable": "isycode.workspace_memory.WorkspaceMemoryOwner.prepare", - "action": null, - "owner": "workspace_memory" - } - ], - "unresolved_dynamic_request_constructors": [] + ], + "dynamic_request_constructors": [ + { + "file": "isycode/action_runtime.py", + "line": 92, + "callable": "isycode.action_runtime._workspace_config_request", + "action": null, + "owner": "workspace_config" + }, + { + "file": "isycode/action_runtime.py", + "line": 2449, + "callable": "isycode.action_runtime.LocalWorkspaceReadOwner._execute", + "action": null, + "owner": "workspace_read" + }, + { + "file": "isycode/broker.py", + "line": 749, + "callable": "isycode.broker.BrokerManagementOwner.request_for", + "action": null, + "owner": "broker_management" + }, + { + "file": "isycode/git_owner.py", + "line": 302, + "callable": "isycode.git_owner.GitOwner._request", + "action": null, + "owner": "workspace_git" + }, + { + "file": "isycode/tailscale_serve.py", + "line": 189, + "callable": "isycode.tailscale_serve.TailscaleServeOwner._prepare", + "action": null, + "owner": "tailscale_serve" + }, + { + "file": "isycode/tui_app_authority.py", + "line": 835, + "callable": "isycode.tui_app_authority.AuthorityMixin._authorize_remote_read", + "action": null, + "owner": "remote_catalog" + }, + { + "file": "isycode/tui_app_workspace.py", + "line": 190, + "callable": "isycode.tui_app_workspace.WorkspaceMixin._workspace_request", + "action": null, + "owner": "workspace_read" + }, + { + "file": "isycode/workspace_memory.py", + "line": 547, + "callable": "isycode.workspace_memory.WorkspaceMemoryOwner.prepare", + "action": null, + "owner": "workspace_memory" + } + ], + "unresolved_dynamic_request_constructors": [] } diff --git a/src/isycode/cat_activity.py b/src/isycode/cat_activity.py index 424cd48..0b01e67 100644 --- a/src/isycode/cat_activity.py +++ b/src/isycode/cat_activity.py @@ -2,67 +2,64 @@ from rich.text import Text -# Each terminal cell holds two by four dots; spaces preserve the silhouette. -# Loading-bar walk of U+130E0. The Noto glyph was mirrored to face right, its -# own leg strokes were swung, and each pose was downsampled with ascii-art -# (width 16, legacy box filter). One dot is dropped wherever a cell would be -# U+28FF, which DejaVu draws as .notdef. The resting sprites stay as they were. +# Original hand-drawn kitten: round head, two ears, short back, four paws +# and an upright curled tail. Sparse outlines keep the 2×4 subcell dots clear. _WALK_FRAMES = ( ( - "................", - "................", - "...........###..", - "..........#####.", - "..........######", - "........###.####", - "....######..##..", - "######.....##...", - "..######....##..", - ".##.##########..", - ".##.#####..####.", - ".####.#.##..#.##", + "........#..#....", + "........####....", + ".......#....#...", + "..##...#.#..##..", + ".#..#..#....#...", + ".#..#...####....", + "..#..####..#....", + "..#.#......#....", + "...##......#....", + "....#.#..#.#....", + "....#.#..#.#....", + "....##...##.....", ), ( - "................", - "................", - "...........###..", - "..........#####.", - "..........######", - "........###.####", - "....######..##..", - "######.....##...", - "..######....##..", - ".##.##########..", - ".##.#####..###..", - ".####.#.#####.##", + "........#..#....", + "........####....", + ".......#....#...", + "..##...#.#..##..", + ".#..#..#....#...", + ".#..#...####....", + "..#..####..#....", + "..#.#......#....", + "...##......#....", + "....#.#..#.#....", + "...#..#..#..#...", + "...#..#...#.#...", ), ( - "................", - "................", - "...........###..", - "..........#####.", - "..........######", - "........###.####", - "....######..##..", - "######.....##...", - "..######....##..", - ".##.##########..", - ".##..####.###...", - ".######.#######.", + "........#..#....", + "........####....", + ".......#....#...", + "..##...#.#..##..", + ".#..#..#....#...", + ".#..#...####....", + "..#..####..#....", + "..#.#......#....", + "...##......#....", + "....#.#..#.#....", + "....#..##..#....", + "....##....##....", ), ( - "................", - "................", - "...........###..", - "..........#####.", - "..........######", - "........###.####", - "....######..##..", - "######.....##...", - "..######..####..", - ".##.##########..", - ".##.#####..#####", - ".####.#.#####.##", + "........#..#....", + "........####....", + ".......#....#...", + "..##...#.#..##..", + ".#..#..#....#...", + ".#..#...####....", + "..#..####..#....", + "..#.#......#....", + "...##......#....", + "....#.#..#.#....", + ".....#.#..#.#...", + ".....#.#..#.#...", ), ) # Whole cat curled up on all three rows: ears, closed eyes, back, belly and a @@ -76,7 +73,7 @@ "#.....##....#...", ".#####......#...", ".#...........#..", - ".#...........#..", + ".#...........#.#", "..#.........#..#", "...#########...#", "...........####.", @@ -132,7 +129,7 @@ def _dots(bitmap: tuple[str, ...]) -> list[str]: def walking_cat(width: int, tick: int) -> Text: width = max(1, width) if width < 10: - return Text(("[:3]" if tick % 2 else "[3:]").ljust(width)[:width], style="#77d8b0") + return Text(("[:3]" if tick % 2 else "[3:]").ljust(width)[:width], style="#f6a34a") travel = width - 10 phase = tick % (2 * travel) if travel else 0 right = phase <= travel @@ -144,7 +141,7 @@ def walking_cat(width: int, tick: int) -> Text: for row, pixels in enumerate(_dots(bitmap)): result.append("[" if row == 1 else " ", style="#9aa3ad") result.append(" " * position) - result.append(pixels, style="#77d8b0") + result.append(pixels, style="#f6a34a") result.append(" " * (travel - position)) result.append("]" if row == 1 else " ", style="#9aa3ad") if row != 2: @@ -222,7 +219,7 @@ def _wrapped_status(label: str, width: int, style: str) -> Text: def sleeping_cat(width: int, caption: str = "Chat ready", caption_style: str = "#9aa3ad") -> Text: - """Cat resting on the caption line, z on its face and a tail behind it. + """Curled kitten with a visible tail and stepped sleep marks when space permits. The walking pose still fills the activity box and bounces when a turn starts. A caption that does not fit beside the cat wraps inside the column. @@ -238,14 +235,16 @@ def sleeping_cat(width: int, caption: str = "Chat ready", caption_style: str = " loaf = _dots(_REST_LOAF) start = cell_len(label) + 1 - if start + 1 + cell_len(loaf[0]) <= width: + sleep_width = 3 if start + 3 + cell_len(loaf[0]) <= width else 1 + if start + sleep_width + cell_len(loaf[0]) <= width: result = Text() for row, pixels in enumerate(loaf): line = Text(label, style=caption_style) if row == 2 else Text(" " * cell_len(label)) line.append(" ") - line.append("z" if row == 1 else " ", style="#77d8b0") - line.append(pixels, style="#77d8b0") - line.append(" " * (width - start - 1 - cell_len(pixels))) + sleep = (" z", " z ", "z ")[row] if sleep_width == 3 else ("z" if row == 1 else " ") + line.append(sleep, style="#f6a34a") + line.append(pixels, style="#f6a34a") + line.append(" " * (width - start - sleep_width - cell_len(pixels))) result.append(line) if row < 2: result.append("\n") @@ -271,11 +270,11 @@ def sleeping_cat(width: int, caption: str = "Chat ready", caption_style: str = " top, body, gap = chosen start = cell_len(label) + gap face = Text(" " * start) - face.append("z" + top, style="#77d8b0") + face.append("z" + top, style="#f6a34a") face.append(" " * (width - start - cell_len("z" + top))) feet = Text(label, style=caption_style) feet.append(" " * gap) - feet.append(" " + body, style="#77d8b0") # the space sits under the z + feet.append(" " + body, style="#f6a34a") # the space sits under the z feet.append(" " * (width - cell_len(label) - gap - cell_len(" " + body))) result = Text() result.append(blank) diff --git a/src/isycode/hive/__init__.py b/src/isycode/hive/__init__.py index f70d9af..a7483a6 100644 --- a/src/isycode/hive/__init__.py +++ b/src/isycode/hive/__init__.py @@ -1,11 +1,9 @@ """ISyCode Hive — on-disk multi-agent coordination layer. A git-backed mailbox system where agents exchange messages via inbox/outbox, -coordinated by a single-committer router. Human-in-the-loop approval surfaces -in the agent's own terminal (ISyCode lane) and can be approved remotely. - -Design mirrors isyco-worlds/src/main/hive.ts but in Python, integrated with -ISyCode's lanes, Sentinel, and the harness compose pipeline. +coordinated by a router. This experimental mailbox is not an execution owner +or an approval channel. Detached external launch is disabled; repair tasks use +normal ISyCode conversation lanes with their existing Authority/Sentinel gates. """ from __future__ import annotations diff --git a/src/isycode/hive/guard.py b/src/isycode/hive/guard.py index e2ec03a..e915dac 100644 --- a/src/isycode/hive/guard.py +++ b/src/isycode/hive/guard.py @@ -1,9 +1,8 @@ """Hive write guard — refuses file writes outside the agent's own hive workspace. -Ported from isyco-worlds/src/main/hiveGuard.ts. Only guards Claude's file tools -(Write, Edit, MultiEdit, NotebookEdit). Shell commands that write files are not -judged from their text. The PreToolUse hook fails OPEN when the shim can't -reach the app's socket, so this is a strong guard, not an airtight one. +Only guards the named Claude file tools. This lexical/physical path check +is an additional coordination constraint, not Workspace Authority, Sentinel +or protection against a hostile process racing filesystem changes. """ from __future__ import annotations @@ -69,8 +68,14 @@ def hive_write_decision(i: HiveWriteInput) -> HiveWriteDecision: hive_root_n = os.path.normpath(norm(i["hiveRoot"])) target_n = os.path.normpath(norm(target)) - if not is_inside(hive_root_n, target_n, i["caseInsensitive"]): + lexical_inside = is_inside(hive_root_n, target_n, i["caseInsensitive"]) + physical_root = os.path.realpath(i["hiveRoot"]) + physical_target = os.path.realpath(target) + physical_inside = is_inside(physical_root, physical_target, i["caseInsensitive"]) + if not lexical_inside and not physical_inside: return {"deny": False, "reason": None} + if not lexical_inside or not physical_inside or os.path.normcase(os.path.abspath(target)) != os.path.normcase(physical_target): + return {"deny": True, "reason": "Hive coordination writes cannot traverse symbolic links."} try: rel = os.path.relpath(target_n, hive_root_n).replace(os.sep, "/") diff --git a/src/isycode/hive/hook_shim.py b/src/isycode/hive/hook_shim.py index 95959e4..77db727 100644 --- a/src/isycode/hive/hook_shim.py +++ b/src/isycode/hive/hook_shim.py @@ -80,16 +80,24 @@ def handle_pre_tool_use(data: dict[str, Any], hive_root: Path, agent_id: str) -> tool_input = data.get("tool_input", {}) cwd = data.get("cwd", os.getcwd()) - # Check write guard for file tools - if tool_name in {"Write", "Edit", "MultiEdit", "NotebookEdit"}: - manager = HiveManager(HiveConfig(harness_home=hive_root.parent)) - manager.ensure_hive() - decision = manager.check_write(tool_name, tool_input, cwd, agent_id) - if decision["deny"]: - return { - "decision": "deny", - "reason": decision["reason"], - } + # Read existing coordination state only. A hook cannot bootstrap or grant it. + try: + from isycode.hive.types import Registry, validate_identifier + validate_identifier(agent_id) + registry = Registry.from_dict(json.loads((hive_root / "registry.json").read_text(encoding="utf-8"))) + agent = registry.agents[agent_id] + if agent.archived: + raise ValueError("Archived Hive agent") + decision = hive_write_decision({ + "tool": tool_name, "toolInput": tool_input, "cwd": cwd, + "agentId": agent_id, "isGod": agent.is_god, + "hiveRoot": str(hive_root), "caseInsensitive": os.name == "nt", + }) + except (OSError, ValueError, TypeError, KeyError): + decision = {"deny": True, "reason": "Hive identity or registry is unavailable."} + if decision["deny"]: + return {"hookSpecificOutput": {"hookEventName": "PreToolUse", + "permissionDecision": "deny", "permissionDecisionReason": decision["reason"]}} # Forward to hive for live status sock_path = os.environ.get("HIVE_SOCK") if sock_path: @@ -101,7 +109,7 @@ def handle_pre_tool_use(data: dict[str, Any], hive_root: Path, agent_id: str) -> "cwd": cwd, "ts": time.time(), }) - return {"decision": "allow"} + return {} def handle_post_tool_use(data: dict[str, Any], agent_id: str) -> dict[str, Any]: diff --git a/src/isycode/hive/manager.py b/src/isycode/hive/manager.py index a69c35d..b61b355 100644 --- a/src/isycode/hive/manager.py +++ b/src/isycode/hive/manager.py @@ -9,6 +9,8 @@ import json import os import subprocess +import shlex +import sys import time import uuid from contextlib import contextmanager @@ -18,6 +20,7 @@ from .guard import hive_write_decision, HiveWriteInput from .router import Router +from .safe_io import checked_path, read_message, create_message from .types import ( AgentMeta, HiveMessage, @@ -26,6 +29,7 @@ Registry, RegistryAgent, SpawnInjection, + validate_identifier, ) @@ -60,7 +64,9 @@ def __init__(self, config: HiveConfig) -> None: # ─── Paths ─── def agent_dir(self, agent_id: str) -> Path: - return self.hive_root / "agents" / agent_id + validate_identifier(agent_id) + path = self.hive_root / "agents" / agent_id + return checked_path(self.hive_root, path) def registry_path(self) -> Path: return self.hive_root / "registry.json" @@ -160,6 +166,8 @@ def upsert_agent(self, meta: AgentMeta) -> RegistryAgent: self.ensure_hive() agent_dir = self.agent_dir(meta.id) agent_dir.mkdir(parents=True, exist_ok=True) + checked_path(self.hive_root, agent_dir / "inbox" / ".done") + checked_path(self.hive_root, agent_dir / "outbox" / ".sent") (agent_dir / "inbox" / ".done").mkdir(parents=True, exist_ok=True) (agent_dir / "outbox" / ".sent").mkdir(parents=True, exist_ok=True) @@ -171,6 +179,8 @@ def upsert_agent(self, meta: AgentMeta) -> RegistryAgent: role = "god" # Write identity.md (refreshed on each spawn) + for relative in ("identity.md", "memory.md", "cursor.json", "inbox", "inbox/.done", "outbox", "outbox/.sent"): + checked_path(self.hive_root, agent_dir / relative) identity = agent_dir / "identity.md" identity.write_text(self._identity_text(meta, role or ""), encoding="utf-8") @@ -271,7 +281,7 @@ def archive_agent(self, agent_id: str) -> None: async def start_router(self) -> None: if self._router is None: - self._router = Router(self.hive_root, self._registry) + self._router = Router(self.hive_root, lambda: self._registry) self._router_task = asyncio.create_task(self._router.start()) elif self._router_task is None or self._router_task.done(): self._router_task = asyncio.create_task(self._router.start()) @@ -325,11 +335,12 @@ def send_message( ) outbox = self.agent_dir(from_agent) / "outbox" + checked_path(self.hive_root, outbox / ".sent") outbox.mkdir(parents=True, exist_ok=True) (outbox / ".sent").mkdir(exist_ok=True) msg_file = outbox / f"{msg.id}.json" - msg_file.write_text(msg.to_jsonl() + "\n", encoding="utf-8") + create_message(self.hive_root, msg_file, msg.to_jsonl() + "\n") self._append_log({ "kind": "message_sent", @@ -351,7 +362,7 @@ def read_inbox(self, agent_id: str, include_done: bool = False) -> list[HiveMess if msg_file.name.startswith("."): continue try: - msgs.append(HiveMessage.from_jsonl(msg_file.read_text(encoding="utf-8"))) + msgs.append(HiveMessage.from_jsonl(read_message(self.hive_root, msg_file))) except Exception: pass if include_done: @@ -359,7 +370,7 @@ def read_inbox(self, agent_id: str, include_done: bool = False) -> list[HiveMess if done.exists(): for msg_file in sorted(done.glob("*.json")): try: - msgs.append(HiveMessage.from_jsonl(msg_file.read_text(encoding="utf-8"))) + msgs.append(HiveMessage.from_jsonl(read_message(self.hive_root, msg_file))) except Exception: pass return msgs @@ -374,7 +385,7 @@ def read_outbox(self, agent_id: str, include_sent: bool = False) -> list[HiveMes if msg_file.name.startswith("."): continue try: - msgs.append(HiveMessage.from_jsonl(msg_file.read_text(encoding="utf-8"))) + msgs.append(HiveMessage.from_jsonl(read_message(self.hive_root, msg_file))) except Exception: pass if include_sent: @@ -382,7 +393,7 @@ def read_outbox(self, agent_id: str, include_sent: bool = False) -> list[HiveMes if sent.exists(): for msg_file in sorted(sent.glob("*.json")): try: - msgs.append(HiveMessage.from_jsonl(msg_file.read_text(encoding="utf-8"))) + msgs.append(HiveMessage.from_jsonl(read_message(self.hive_root, msg_file))) except Exception: pass return msgs @@ -393,10 +404,12 @@ def mark_inbox_read(self, agent_id: str, msg_ids: list[str]) -> None: done = inbox / ".done" done.mkdir(exist_ok=True) for msg_id in msg_ids: - src = inbox / f"{msg_id}.json" + validate_identifier(msg_id) + src = checked_path(self.hive_root, inbox / f"{msg_id}.json") + checked_path(self.hive_root, done) if src.exists(): try: - src.rename(done / msg_id.name) + src.rename(done / src.name) except Exception: pass @@ -462,72 +475,23 @@ def list_tasks(self) -> list[HiveTask]: def build_spawn_injection(self, meta: AgentMeta, extra_writable: list[str] | None = None) -> SpawnInjection: """Build provider-specific args + env that make the process hive-aware.""" + provider = (meta.provider or "claude").lower() + if provider != "claude": + raise ValueError(f"Hive hooks for {provider} are not supported") self.ensure_hive() + if meta.id not in self.registry.agents: + raise ValueError("Register the Hive agent before building hook settings") agent_dir = self.agent_dir(meta.id) - root = self.hive_root - - env: dict[str, str] = { - "AGENT_ID": meta.id, - "AGENT_NAME": meta.name, - "HIVE_ROOT": str(root), - "AGENT_DIR": str(agent_dir), - } - - provider = (meta.provider or "claude").lower() - - if provider in HIVE_AWARE_PROVIDERS: - # Claude Code: --append-system-prompt + --settings with hook config - env["HIVE_SOCK"] = self.sock_path() or "" - settings_path = agent_dir / "settings.json" - settings_path.write_text(json.dumps(self._hook_settings(meta, str(agent_dir), extra_writable)), encoding="utf-8") - args = ["--append-system-prompt", self._injected_prompt(meta, str(agent_dir), str(root)), "--settings", str(settings_path)] - return SpawnInjection(args=args, env=env) - - if provider in HOOK_PROVIDERS: - # Hook-based providers (codex, opencode, gemini, pi, grok, crush) - env["HIVE_SOCK"] = self.sock_path() or "" - preset = self._provider_preset(provider) - prompt = self._injected_prompt(meta, str(agent_dir), str(root)) - args = [] - degraded = None - - if provider == "codex": - # Codex needs isolated CODEX_HOME and --dangerously-bypass-hook-trust - codex_home = agent_dir / ".codex" - codex_home.mkdir(exist_ok=True) - (codex_home / "config.toml").write_text( - self._codex_hooks_toml(str(self.sock_path())), encoding="utf-8" - ) - env["CODEX_HOME"] = str(codex_home) - args.append("--dangerously-bypass-hook-trust") - # Extra writable dirs for auto mode - for d in self._sandbox_writable_dirs(meta, str(agent_dir), str(root), extra_writable): - args.extend(["--add-dir", d]) - - elif provider == "crush": - # Crush: seedDelivery = type-into-tui, no positional prompt - env["HIVE_NODE"] = "node" # fallback - return SpawnInjection(args=[], env=env, seed_prompt=prompt) - - elif provider == "grok": - pass # grok takes positional prompt - - if preset.get("initialPromptFlag"): - return SpawnInjection(args=args + [preset["initialPromptFlag"], prompt], env=env) - if preset.get("positionalInitialPrompt"): - return SpawnInjection(args=args + [prompt], env=env) - return SpawnInjection(args=args, env=env, seed_prompt=prompt) - - if provider in PROXY_PROVIDERS: - # Proxy-based (qwen) — would need a sidecar. For now, degrade. - return SpawnInjection( - args=[], - env=env, - degraded=f"{meta.name} (provider {provider}) runs without hive events: proxy sidecar not implemented in Python hive yet." - ) - - # Unknown provider — spawn bare - return SpawnInjection(args=[], env=env) + settings_path = checked_path(self.hive_root, agent_dir / "settings.json") + settings_path.write_text(json.dumps(self._hook_settings(meta, str(agent_dir), extra_writable)), encoding="utf-8") + return SpawnInjection( + args=["--append-system-prompt", self._injected_prompt(meta, str(agent_dir), str(self.hive_root)), + "--settings", str(settings_path)], + env={"AGENT_ID": meta.id, "AGENT_NAME": meta.name, + "HIVE_ROOT": str(self.hive_root), "AGENT_DIR": str(agent_dir), + "HIVE_SOCK": self.sock_path() or ""}, + degraded="Coordination hooks only; not an ISyCode execution owner or sandbox.", + ) # ─── Hook Settings / Prompts ─── @@ -569,10 +533,9 @@ def _hook_settings( } def _hook_shim_path(self) -> str: - """Path to the hook shim script (cth-hook.cjs equivalent in Python).""" - # We'll implement the shim as a Python module invoked via the bundled node trick - # For now, return a placeholder that the TUI will resolve - return str(self.hive_root / "bin" / "isyco-hook") + """Invoke the installed Python source, with platform-correct shell quoting.""" + args = [sys.executable, str(Path(__file__).with_name("hook_shim.py").resolve())] + return subprocess.list2cmdline(args) if os.name == "nt" else shlex.join(args) def _injected_prompt(self, meta: AgentMeta, agent_dir: str, root: str) -> str: """The system prompt injected into the agent at spawn.""" @@ -626,14 +589,14 @@ def _injected_prompt(self, meta: AgentMeta, agent_dir: str, root: str) -> str: ## Human-in-the-loop - Permission prompts surface in YOUR terminal (ISyCode lane). -- Approve remotely via the god agent or the TUI. +- A Hive message or god role cannot approve an ISyCode action. - `needs_human: true` routes to god for explicit approval. ## Guardrails - You may ONLY write to: your `memory.md`, `inbox/`, `outbox/`, `tasks.json`. - God may also write `board.md` and `spawn-requests/`. -- Any other write inside the hive root is DENIED by the guard. +- The coordination file guard rejects other Hive paths; it does not sandbox shell commands. - To reach another agent, write a message to your outbox. ## Your Mission @@ -658,24 +621,6 @@ def _identity_text(self, meta: AgentMeta, role: str) -> str: --- *This file is refreshed on every spawn by the hive manager.* -""" - - def _provider_preset(self, provider: str) -> dict[str, Any]: - presets = { - "codex": {"initialPromptFlag": None, "positionalInitialPrompt": True, "hookBridge": "hooks", "shim": "codex"}, - "opencode": {"initialPromptFlag": None, "positionalInitialPrompt": True, "hookBridge": "hooks", "shim": "opencode"}, - "gemini": {"initialPromptFlag": None, "positionalInitialPrompt": True, "hookBridge": "hooks", "shim": "gemini"}, - "pi": {"initialPromptFlag": None, "positionalInitialPrompt": True, "hookBridge": "hooks", "shim": "pi"}, - "grok": {"initialPromptFlag": None, "positionalInitialPrompt": True, "hookBridge": "hooks", "shim": "grok"}, - "crush": {"initialPromptFlag": None, "positionalInitialPrompt": False, "hookBridge": "hooks", "shim": "crush", "seedDelivery": "type-into-tui"}, - } - return presets.get(provider, {"initialPromptFlag": None, "positionalInitialPrompt": True}) - - def _codex_hooks_toml(self, sock: str) -> str: - return f"""[hooks] -pre_tool_use = [{{ command = "node {self.hive_root}/bin/isyco-hook pre-tool-use" }}] -post_tool_use = [{{ command = "node {self.hive_root}/bin/isyco-hook post-tool-use" }}] -stop = [{{ command = "node {self.hive_root}/bin/isyco-hook stop" }}] """ def _sandbox_writable_dirs( @@ -734,12 +679,15 @@ def _append_log(self, entry: dict[str, Any]) -> None: def check_write(self, tool: str, tool_input: dict[str, Any], cwd: str, agent_id: str) -> dict[str, Any]: """Check if a file write is allowed in the hive.""" + agent = self._registry.agents.get(agent_id) + if agent is None or agent.archived: + return {"deny": True, "reason": "Unknown or archived Hive agent."} inp: HiveWriteInput = { "tool": tool, "toolInput": tool_input, "cwd": cwd, "agentId": agent_id, - "isGod": self._registry.agents.get(agent_id, RegistryAgent(id="", name="")).is_god, + "isGod": agent.is_god, "hiveRoot": str(self.hive_root), "caseInsensitive": os.name == "nt", } @@ -851,7 +799,9 @@ def check_write(self, tool: str, tool_input: dict[str, Any], cwd: str, agent_id: - `HIVE_SOCK` (Unix socket for hooks) - Provider-specific args/env: - **Claude**: `--append-system-prompt ` + `--settings ` - - **Codex**: positional prompt + `--dangerously-bypass-hook-trust` + `--add-dir` - - **Crush**: `seedPrompt` (typed into TUI after boot) - - **Proxy (qwen)**: `HIVE_PROXY_SESSION`, loopback proxy sidecar + - Other CLIs are unsupported; no trust-bypass flags or fake hook configs are emitted. + +External process launch is disabled. A coordination hook is not a sandbox, +Workspace Authority grant, Sentinel decision or approval channel. Repairs run +in an ordinary ISyCode conversation using its selected model and execution owners. """ \ No newline at end of file diff --git a/src/isycode/hive/router.py b/src/isycode/hive/router.py index 06ed9b9..d691a59 100644 --- a/src/isycode/hive/router.py +++ b/src/isycode/hive/router.py @@ -11,9 +11,10 @@ import shutil import time from pathlib import Path -from typing import Any +from typing import Any, Callable -from .types import HiveMessage, MessageAct, Registry +from .types import HiveMessage, MessageAct, Registry, validate_identifier +from .safe_io import checked_path, read_message, create_message class Router: @@ -22,13 +23,18 @@ class Router: HOP_CAP = 12 SCAN_INTERVAL = 1.0 # seconds - def __init__(self, hive_root: Path, registry: Registry) -> None: + def __init__(self, hive_root: Path, registry: Registry | Callable[[], Registry]) -> None: self.hive_root = hive_root - self.registry = registry + self._registry_source = registry self._task: asyncio.Task | None = None self._running = False self._last_route_at = 0.0 + @property + def registry(self) -> Registry: + source = self._registry_source + return source() if callable(source) else source + async def start(self) -> None: if self._running: return @@ -59,39 +65,45 @@ async def _run_loop(self) -> None: async def _route_once(self) -> None: """Scan all agents' outbox/ and deliver messages.""" agents_dir = self.hive_root / "agents" + checked_path(self.hive_root, agents_dir) if not agents_dir.exists(): return for agent_dir in agents_dir.iterdir(): - if not agent_dir.is_dir(): + if agent_dir.is_symlink() or not agent_dir.is_dir(): continue agent_id = agent_dir.name + if agent_id not in self.registry.agents or self.registry.agents[agent_id].archived: + continue outbox = agent_dir / "outbox" + if outbox.is_symlink(): + continue if not outbox.exists(): continue for msg_file in sorted(outbox.glob("*.json")): if msg_file.name.startswith("."): continue - await self._deliver_message(agent_id, msg_file) - - # Also check .sent for any strays - sent = outbox / ".sent" - if sent.exists(): - for msg_file in sorted(sent.glob("*.json")): + try: await self._deliver_message(agent_id, msg_file) + except (ValueError, OSError): + # Unsafe recipient paths stay pending, without touching the target. + continue self._last_route_at = time.time() async def _deliver_message(self, from_agent_id: str, msg_file: Path) -> None: try: - text = msg_file.read_text(encoding="utf-8") + validate_identifier(from_agent_id) + text = read_message(self.hive_root, msg_file) msg = HiveMessage.from_jsonl(text) - except Exception: - # Malformed → quarantine - quarantine = self.hive_root / "quarantine" - quarantine.mkdir(exist_ok=True) - shutil.move(str(msg_file), str(quarantine / msg_file.name)) + if msg.from_agent != from_agent_id or from_agent_id not in self.registry.agents: + raise ValueError("Hive sender does not match outbox owner") + if msg_file.name != f"{msg.id}.json": + raise ValueError("Hive envelope ID does not match filename") + except (ValueError, TypeError, KeyError, OSError): + # Keep the source intact for diagnosis; never follow an untrusted path + # while trying to quarantine it. It cannot be delivered. return # Check hop limit @@ -110,8 +122,13 @@ async def _deliver_message(self, from_agent_id: str, msg_file: Path) -> None: if to_agent_id not in self.registry.agents: continue to_agent = self.registry.agents[to_agent_id] + if to_agent.archived: + continue + validate_identifier(to_agent_id) inbox = self.hive_root / "agents" / to_agent_id / "inbox" + checked_path(self.hive_root, inbox) inbox.mkdir(parents=True, exist_ok=True) + checked_path(self.hive_root, inbox / ".done") (inbox / ".done").mkdir(exist_ok=True) # Increment hops and write @@ -130,7 +147,8 @@ async def _deliver_message(self, from_agent_id: str, msg_file: Path) -> None: created_at=msg.created_at, ) dest = inbox / f"{msg.id}.json" - dest.write_text(delivered.to_jsonl() + "\n", encoding="utf-8") + if not (inbox / ".done" / dest.name).exists(): + create_message(self.hive_root, dest, delivered.to_jsonl() + "\n") # Archive from sender's outbox self._archive_sent(from_agent_id, msg_file, "delivered") @@ -157,6 +175,9 @@ def _archive_sent(self, agent_id: str, msg_file: Path, reason: str) -> None: """Move message from outbox/ to outbox/.sent/.""" agent_dir = self.hive_root / "agents" / agent_id sent_dir = agent_dir / "outbox" / ".sent" + checked_path(self.hive_root, sent_dir) + checked_path(self.hive_root, msg_file) + checked_path(self.hive_root, sent_dir / msg_file.name) sent_dir.mkdir(parents=True, exist_ok=True) try: shutil.move(str(msg_file), str(sent_dir / msg_file.name)) diff --git a/src/isycode/hive/safe_io.py b/src/isycode/hive/safe_io.py new file mode 100644 index 0000000..04b52b3 --- /dev/null +++ b/src/isycode/hive/safe_io.py @@ -0,0 +1,72 @@ +"""No-link paths for private Hive coordination (not workspace authorization).""" +from __future__ import annotations + +import os +import stat +from contextlib import contextmanager +from pathlib import Path + + +def checked_path(root: Path, path: Path) -> Path: + root = root.absolute() + path = path.absolute() + relative = path.relative_to(root) + if '..' in relative.parts: + raise ValueError('Hive path traversal') + for component in (root, *root.parents): + if component.is_symlink() or getattr(component, "is_junction", lambda: False)(): + raise ValueError('Hive root traverses a symbolic link') + current = root + for part in relative.parts: + current = current / part + if current.is_symlink() or getattr(current, "is_junction", lambda: False)(): + raise ValueError('Hive path traverses a symbolic link') + return path + + +@contextmanager +def directory(root: Path, path: Path): + """Pin directory descriptors on POSIX to prevent parent-link swaps.""" + checked_path(root, path) + if os.name == 'nt': + yield None + return + flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW + fd = os.open(root, flags) + try: + for part in path.absolute().relative_to(root.absolute()).parts: + next_fd = os.open(part, flags, dir_fd=fd) + os.close(fd) + fd = next_fd + yield fd + finally: + os.close(fd) + + +def read_message(root: Path, path: Path, limit: int = 2000000) -> str: + checked_path(root, path) + with directory(root, path.parent) as parent: + flags = os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) | getattr(os, 'O_NONBLOCK', 0) + fd = os.open(path if parent is None else path.name, flags, **({} if parent is None else {'dir_fd': parent})) + with os.fdopen(fd, 'rb') as stream: + if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode): + raise ValueError('Hive messages must be regular files') + raw = stream.read(limit + 1) + if len(raw) > limit: + raise ValueError('Oversized Hive message') + return raw.decode('utf-8') + + +def create_message(root: Path, path: Path, text: str) -> bool: + """Exclusive delivery preserves an existing message instead of overwriting it.""" + checked_path(root, path) + with directory(root, path.parent) as parent: + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_NOFOLLOW', 0) + try: + fd = os.open(path if parent is None else path.name, flags, 0o600, + **({} if parent is None else {'dir_fd': parent})) + except FileExistsError: + return False + with os.fdopen(fd, 'w', encoding='utf-8') as stream: + stream.write(text) + return True diff --git a/src/isycode/hive/spawn.py b/src/isycode/hive/spawn.py index 808e611..80f8388 100644 --- a/src/isycode/hive/spawn.py +++ b/src/isycode/hive/spawn.py @@ -1,17 +1,12 @@ -"""Hive agent spawning — runs in a separate module to avoid TUI app scanning restrictions. +"""External Hive launch is unavailable until an execution owner can enforce it. -This module is NOT scanned by test_harness_tui.py's subprocess check because -it's not a tui_app_*.py file. +Coordination hooks are not a sandbox or an Authority grant. TUI repair tasks use +ISyCode's normal conversation owner instead of starting detached external CLIs. """ from __future__ import annotations -import asyncio -import os -import shutil from dataclasses import dataclass -from typing import Any - -from .manager import HiveManager, HiveConfig, AgentMeta +from .manager import HiveManager from .hook_server import HookServer @@ -26,78 +21,14 @@ class SpawnResult: async def spawn_hive_agent( manager: HiveManager, - hook_server: HookServer, + hook_server: HookServer | None, provider: str, compose: str, workspace_root: str | None = None, ) -> SpawnResult: - """Spawn a hive agent process with the given compose as initial prompt. - - Returns immediately after spawning (fire-and-forget). The agent runs - with hive coordination (inbox/outbox, git-backed). - """ - # Ensure hook server + router are running - await hook_server.start() - await manager.start_router() - - # Create agent meta - agent_id = f"repair-{int(asyncio.get_event_loop().time() * 1000)}" - meta = AgentMeta( - id=agent_id, - name=f"Repair Agent ({provider})", - provider=provider, - role="harness repair", - capabilities=["file_edit", "shell", "web_fetch"], - cwd=workspace_root or os.getcwd(), - ) - - # Upsert agent in hive - manager.upsert_agent(meta) - - # Build spawn injection - injection = manager.build_spawn_injection(meta) - - # Resolve provider executable - executable = shutil.which(provider) - if not executable: - return SpawnResult( - agent_id=agent_id, - provider=provider, - pid=None, - hive_root=str(manager.hive_root), - error=f"{provider} CLI not found in PATH", - ) - - # Build command - cmd = [executable, *injection.args] - env = os.environ.copy() - env.update(injection.env) - - # Spawn detached process - try: - proc = await asyncio.create_subprocess_exec( - *cmd, - env=env, - cwd=meta.cwd or os.getcwd(), - start_new_session=True, - stdin=asyncio.subprocess.DEVNULL, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.DEVNULL, - ) - return SpawnResult( - agent_id=agent_id, - provider=provider, - pid=proc.pid, - hive_root=str(manager.hive_root), - ) - except Exception as e: - return SpawnResult( - agent_id=agent_id, - provider=provider, - pid=None, - hive_root=str(manager.hive_root), - error=str(e), - ) + """Reject unowned external execution without initializing files or services.""" + return SpawnResult("", provider, None, str(manager.hive_root), + "External Hive launch requires a bounded execution owner; use an ISyCode repair conversation.") def spawn_hive_agent_sync( @@ -106,5 +37,6 @@ def spawn_hive_agent_sync( compose: str, workspace_root: str | None = None, ) -> SpawnResult: - """Synchronous wrapper for non-async contexts.""" - return asyncio.run(spawn_hive_agent(manager, None, provider, compose, workspace_root)) \ No newline at end of file + """Same fail-closed result for synchronous callers.""" + return SpawnResult("", provider, None, str(manager.hive_root), + "External Hive launch requires a bounded execution owner; use an ISyCode repair conversation.") diff --git a/src/isycode/hive/types.py b/src/isycode/hive/types.py index 7fbdea3..1098a8c 100644 --- a/src/isycode/hive/types.py +++ b/src/isycode/hive/types.py @@ -2,6 +2,7 @@ from __future__ import annotations import json +import re from dataclasses import dataclass, field, asdict from datetime import datetime from enum import Enum @@ -9,6 +10,13 @@ from typing import Any, Literal, TypedDict +def validate_identifier(value: str) -> str: + """Identifiers are bounded single path components, never filesystem paths.""" + if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", value): + raise ValueError("Invalid Hive identifier") + return value + + class MessageAct(str, Enum): REQUEST = "request" INFORM = "inform" @@ -34,6 +42,21 @@ class HiveMessage: needs_human: bool = False created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat() + "Z") + def __post_init__(self) -> None: + for value in (self.id, self.conversation, self.from_agent, self.to_agent): + validate_identifier(value) + if self.in_reply_to is not None: + validate_identifier(self.in_reply_to) + if not isinstance(self.act, MessageAct): + raise ValueError("Invalid message act") + if type(self.hops) is not int or not 0 <= self.hops <= 12: + raise ValueError("Invalid hop count") + if type(self.requires_reply) is not bool or type(self.needs_human) is not bool: + raise ValueError("Invalid message flags") + for text, limit in ((self.subject, 4096), (self.body, 262144), (self.created_at, 128)): + if not isinstance(text, str) or len(text.encode("utf-8")) > limit: + raise ValueError("Invalid or oversized message text") + def to_dict(self) -> dict[str, Any]: d = asdict(self) d["act"] = self.act.value @@ -121,6 +144,10 @@ class AgentMeta: is_assistant: bool = False # prep assistant — send-only, excluded from broadcast + def __post_init__(self) -> None: + validate_identifier(self.id) + + @dataclass(frozen=True) class RegistryAgent(AgentMeta): status: Literal["idle", "working", "blocked", "gone"] = "idle" @@ -144,10 +171,19 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls, d: dict[str, Any]) -> Registry: + if not isinstance(d, dict) or not isinstance(d.get("agents", {}), dict): + raise ValueError("Invalid Hive registry shape") agents = { k: RegistryAgent(**v) for k, v in d.get("agents", {}).items() } - return cls(god_id=d.get("godId"), agents=agents) + for key, agent in agents.items(): + validate_identifier(key) + if key != agent.id: + raise ValueError("Registry key does not match agent ID") + god_id = d.get("godId") + if god_id is not None: + validate_identifier(god_id) + return cls(god_id=god_id, agents=agents) @dataclass(frozen=True) diff --git a/src/isycode/tui_app_sessions.py b/src/isycode/tui_app_sessions.py index 32e1a4c..f410a74 100644 --- a/src/isycode/tui_app_sessions.py +++ b/src/isycode/tui_app_sessions.py @@ -718,61 +718,12 @@ async def _launch_repair_conversation(self, brief: str) -> None: self.run_worker(self._refresh_work_list(), group="work-list") async def _launch_hive_agent(self, brief: str) -> None: - """Spawn a hive agent to execute the repair compose. - - The agent runs as a separate CLI process (Claude Code, Codex, etc.) - with hive coordination (inbox/outbox, git-backed). The user chooses - the provider. The agent receives the compose as its initial prompt. - """ + """Compatibility route: repairs run through the existing authorized lane.""" if not brief.strip(): self._append(" Repair compose is empty.", YELLOW) return - - # Provider selection for the hive agent - from isycode.tui_screens_harness import ModelsScreen - model_screen = ModelsScreen(provider_scope=None) - selected = await self._await_screen(model_screen) - if not selected: - return - if isinstance(selected, dict): - value = selected.get("value", "") - if "|" in value: - provider, model = value.split("|", 1) - else: - provider = value - model = "" - else: - self._append(" Invalid model selection.", YELLOW) - return - - # Create hive manager - from isycode.platform_storage import storage - from isycode.workspace_setup import state_root - from isycode.hive import spawn_hive_agent, HiveManager, HiveConfig - from isycode.hive.hook_server import HookServer - - hive_root = state_root() / "hive" - manager = HiveManager(HiveConfig(harness_home=hive_root)) - manager.ensure_hive() - - hook_server = HookServer(manager) - - workspace_root = str(self._workspace_root) if hasattr(self, "_workspace_root") and self._workspace_root else None - - self._append(f" Spawning hive agent with {provider}...", CYAN) - result = await spawn_hive_agent( - manager=manager, - hook_server=hook_server, - provider=provider, - compose=brief, - workspace_root=workspace_root, - ) - - if result.error: - self._append(f" Failed to spawn hive agent: {result.error}", YELLOW) - else: - self._append(f" Hive agent {result.agent_id} spawned (PID {result.pid}) with {provider}.", GREEN) - self._append(f" Compose injected as initial prompt. Check hive at {result.hive_root}.", CYAN) + self._append(" External Hive launch is unavailable; opening an ISyCode repair conversation with the current model.", YELLOW) + await self._launch_repair_conversation(brief) def _conversation_status(self) -> str: if isinstance(self.screen, (ApprovalScreen, ContextAccessScreen, AgentQuestionScreen)): diff --git a/src/isycode/tui_screens_harness.py b/src/isycode/tui_screens_harness.py index e558ea8..ace32d9 100644 --- a/src/isycode/tui_screens_harness.py +++ b/src/isycode/tui_screens_harness.py @@ -551,7 +551,7 @@ def compose(self) -> ComposeResult: with Horizontal(id="repair-confirm-actions", classes="isy-dialog-actions"): yield Button(tr("Cancel · Esc"), id="repair-cancel") yield Button(tr("Launch repair conversation"), id="repair-lane", variant="primary") - yield Button(tr("Launch in Hive"), id="repair-hive", variant="success") + yield Button(tr("External Hive · unavailable"), id="repair-hive", disabled=True) def _render_body(self) -> Text: body = Text() @@ -572,7 +572,7 @@ def _render_body(self) -> Text: body.append("\n") body.append("\nChoose execution mode:\n", style="#f6c77b") body.append(" • Lane: new ISyCode conversation in this window\n", style="#aeb6c5") - body.append(" • Hive: spawn a hive agent (multi-agent, git-backed coordination)\n", style="#aeb6c5") + body.append(" • External Hive: unavailable until execution-owner enforcement is implemented\n", style="#aeb6c5") return body def on_button_pressed(self, event: Button.Pressed) -> None: diff --git a/tests/test_cat_activity.py b/tests/test_cat_activity.py index 7582c2f..8a7d752 100644 --- a/tests/test_cat_activity.py +++ b/tests/test_cat_activity.py @@ -33,7 +33,7 @@ def test_sleeping_cat_sits_beside_the_caption_with_z_on_its_face(): face = rows[1] z_at = face.index("z") braille = [i for i, char in enumerate(face) if "\u2800" < char <= "\u28ff"] - assert braille and z_at == braille[0] - 1 + assert braille and 1 <= braille[0] - z_at <= 2 assert "z" not in face[braille[-1] + 1:] assert not rows[2].rstrip().endswith("z") tail = [i for i, char in enumerate(rows[2]) if "\u2800" < char <= "\u28ff"] @@ -75,3 +75,11 @@ def test_resting_cat_fills_all_three_rows_when_the_column_has_room(): ink = [[i for i, char in enumerate(row) if "⠀" < char <= "⣿"] for row in rows] assert all(ink), "ears, body and tail each get a row" assert all(i > len("Chat ready") for row in ink for i in row) + + +def test_sleep_symbols_climb_away_from_the_face_without_widening_track(): + from isycode.cat_activity import sleeping_cat + rows = sleeping_cat(30).plain.splitlines() + positions = [row.index('z') for row in rows] + assert positions[0] == positions[1] + 1 == positions[2] + 2 + assert all(row.count('z') == 1 and cell_len(row) == 30 for row in rows) diff --git a/tests/test_hive_regressions.py b/tests/test_hive_regressions.py new file mode 100644 index 0000000..d0429a3 --- /dev/null +++ b/tests/test_hive_regressions.py @@ -0,0 +1,201 @@ +"""Regression controls for the post-merge Hive audit.""" +import asyncio +import json +from pathlib import Path + +import pytest + +from isycode.hive import HiveManager, HiveConfig, AgentMeta, MessageAct +from isycode.hive.router import Router +from isycode.hive.spawn import spawn_hive_agent + + +def hive(tmp_path): + manager = HiveManager(HiveConfig(harness_home=tmp_path, auto_commit=False)) + for name in ('alice', 'bob'): + manager.upsert_agent(AgentMeta(id=name, name=name, cwd=str(tmp_path))) + return manager + + +@pytest.mark.asyncio +async def test_acknowledged_message_is_not_replayed_after_router_restart(tmp_path): + manager = hive(tmp_path) + msg = manager.send_message('alice', 'bob', MessageAct.INFORM, 'hello', 'body') + await Router(manager.hive_root, manager.registry)._route_once() + manager.mark_inbox_read('bob', [msg.id]) + assert manager.read_inbox('bob') == [] + assert len(manager.read_inbox('bob', include_done=True)) == 1 + await Router(manager.hive_root, manager.registry)._route_once() + assert manager.read_inbox('bob') == [] + + +@pytest.mark.asyncio +@pytest.mark.parametrize('field,value', [('id', '../escaped'), ('id', '/tmp/escaped'), ('from', 'mallory'), ('hops', -1), ('requires_reply', 'yes')]) +async def test_router_rejects_untrusted_envelope(tmp_path, field, value): + manager = hive(tmp_path) + msg = manager.send_message('alice', 'bob', MessageAct.INFORM, 'hello', 'body') + source = manager.agent_dir('alice') / 'outbox' / f'{msg.id}.json' + envelope = json.loads(source.read_text()) + envelope[field] = value + source.write_text(json.dumps(envelope)) + await Router(manager.hive_root, manager.registry)._route_once() + assert manager.read_inbox('bob') == [] + assert not (manager.agent_dir('bob') / 'escaped.json').exists() + + +def test_write_guard_rejects_symlink_into_peer(tmp_path): + manager = hive(tmp_path) + link = manager.agent_dir('alice') / 'outbox' / 'peer' + try: + link.symlink_to(manager.agent_dir('bob'), target_is_directory=True) + except OSError: + pytest.skip('symlinks unavailable') + decision = manager.check_write('Write', {'file_path': str(link / 'identity.md')}, str(tmp_path), 'alice') + assert decision['deny'] + + +@pytest.mark.asyncio +async def test_router_observes_agents_registered_after_start(tmp_path): + manager = HiveManager(HiveConfig(harness_home=tmp_path, auto_commit=False)) + manager.ensure_hive() + await manager.start_router() + try: + manager.upsert_agent(AgentMeta(id='alice', name='alice', cwd=str(tmp_path))) + manager.upsert_agent(AgentMeta(id='bob', name='bob', cwd=str(tmp_path))) + manager.send_message('alice', 'bob', MessageAct.INFORM, 'hello', 'body') + await manager._router._route_once() + assert len(manager.read_inbox('bob')) == 1 + finally: + await manager.stop_router() + + +@pytest.mark.asyncio +async def test_unowned_external_spawn_fails_without_side_effects(tmp_path): + manager = HiveManager(HiveConfig(harness_home=tmp_path)) + result = await spawn_hive_agent(manager, None, 'python3', 'do work', str(tmp_path)) + assert result.pid is None + assert result.error + assert not manager.hive_root.exists() + + +@pytest.mark.asyncio +async def test_hive_repair_uses_authorized_lane_with_exact_compose(): + from isycode.tui_app_sessions import SessionMixin + class App: + def __init__(self): + self.messages = [] + self.briefs = [] + def _append(self, text, color): + self.messages.append(text) + async def _launch_repair_conversation(self, brief): + self.briefs.append(brief) + app = App() + await SessionMixin._launch_hive_agent(app, 'exact repair task') + assert app.briefs == ['exact repair task'] + + +def test_generated_hook_runs_real_python_shim_from_spaced_directory(tmp_path): + import os + import shlex + import subprocess + manager = hive(tmp_path / 'with spaces') + meta = AgentMeta(id='alice', name='Alice', provider='claude', cwd=str(tmp_path)) + injection = manager.build_spawn_injection(meta) + settings = json.loads((manager.agent_dir('alice') / 'settings.json').read_text()) + command = settings['hooks']['PreToolUse'][0]['hooks'][0]['command'] + argv = command if os.name == 'nt' else shlex.split(command) + result = subprocess.run(argv, input=json.dumps({'tool_name': 'Write', 'tool_input': { + 'file_path': str(manager.agent_dir('bob') / 'identity.md')}, 'cwd': str(tmp_path)}), + text=True, capture_output=True, env={**os.environ, **injection.env}, timeout=5) + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout)['hookSpecificOutput']['permissionDecision'] == 'deny' + + +def test_unsupported_hook_provider_does_not_emit_trust_bypass(tmp_path): + manager = HiveManager(HiveConfig(harness_home=tmp_path)) + with pytest.raises(ValueError, match='not supported'): + manager.build_spawn_injection(AgentMeta(id='agent', name='agent', provider='codex')) + assert not manager.hive_root.exists() + + +@pytest.mark.asyncio +async def test_router_does_not_overwrite_existing_inbox_message(tmp_path): + manager = hive(tmp_path) + msg = manager.send_message('alice', 'bob', MessageAct.INFORM, 'hello', 'original') + router = Router(manager.hive_root, manager.registry) + await router._route_once() + archived = manager.agent_dir('alice') / 'outbox' / '.sent' / f'{msg.id}.json' + envelope = json.loads(archived.read_text()) + envelope['body'] = 'replacement' + (archived.parent.parent / archived.name).write_text(json.dumps(envelope)) + await router._route_once() + assert manager.read_inbox('bob')[0].body == 'original' + + +@pytest.mark.asyncio +async def test_router_rejects_linked_recipient_inbox(tmp_path): + manager = hive(tmp_path) + outside = tmp_path / 'outside' + outside.mkdir() + # Register recipient without bootstrap so its first inbox can be a link. + from isycode.hive.types import RegistryAgent + carol = manager.agent_dir('carol') + carol.mkdir() + try: + (carol / 'inbox').symlink_to(outside, target_is_directory=True) + except OSError: + pytest.skip('symlinks unavailable') + msg = manager.send_message('alice', 'bob', MessageAct.INFORM, 'hello', 'body') + source = manager.agent_dir('alice') / 'outbox' / f'{msg.id}.json' + envelope = json.loads(source.read_text()) + manager.registry.agents['carol'] = RegistryAgent(id='carol', name='carol') + envelope['to'] = 'carol' + source.write_text(json.dumps(envelope)) + router = Router(manager.hive_root, lambda: manager.registry) + await router._route_once() + assert list(outside.iterdir()) == [] + + +def test_agent_ids_cannot_escape_hive(tmp_path): + manager = HiveManager(HiveConfig(harness_home=tmp_path)) + with pytest.raises(ValueError): + manager.upsert_agent(AgentMeta(id='../escape', name='bad')) + assert not (tmp_path / 'escape').exists() + + +def test_upsert_does_not_follow_linked_identity(tmp_path): + manager = hive(tmp_path) + # Use a new agent directory to avoid replacing any existing fixture file. + directory = manager.agent_dir('carol') + directory.mkdir() + private = tmp_path / 'private.txt' + private.write_text('untouched') + try: + (directory / 'identity.md').symlink_to(private) + except OSError: + pytest.skip('symlinks unavailable') + with pytest.raises(ValueError): + manager.upsert_agent(AgentMeta(id='carol', name='carol')) + assert private.read_text() == 'untouched' + + +@pytest.mark.asyncio +@pytest.mark.parametrize('body', ['\n' * 200000, '\0' * 262144]) +async def test_escaped_message_body_still_delivers_within_documented_limit(tmp_path, body): + manager = hive(tmp_path) + manager.send_message('alice', 'bob', MessageAct.INFORM, '\0' * 4096, body) + await Router(manager.hive_root, manager.registry)._route_once() + messages = manager.read_inbox('bob') + assert len(messages) == 1 + assert messages[0].body == body + + +@pytest.mark.parametrize('registry', [[], {'agents': []}]) +def test_malformed_registry_denies_instead_of_crashing_hook(tmp_path, registry): + from isycode.hive.hook_shim import handle_pre_tool_use + root = tmp_path / 'hive' + root.mkdir() + (root / 'registry.json').write_text(json.dumps(registry)) + response = handle_pre_tool_use({'tool_name': 'Write', 'tool_input': { + 'file_path': str(root / 'board.md')}, 'cwd': str(tmp_path)}, root, 'alice') + assert response['hookSpecificOutput']['permissionDecision'] == 'deny' diff --git a/tests/test_session_continuity_tui.py b/tests/test_session_continuity_tui.py index 95fc434..535b253 100644 --- a/tests/test_session_continuity_tui.py +++ b/tests/test_session_continuity_tui.py @@ -319,7 +319,7 @@ async def complete(provider, messages, **kwargs): async def scenario(): app = TUIApp() async with app.run_test() as pilot: - await pilot.pause() + await app.wait_for_ready() app._persist_chat_message('user', 'Old conversation.') app._history = [{'role': 'user', 'content': 'Old conversation.'}, {'role': 'assistant', 'content': 'Old answer.'}] * 4 From 7f6edca99fdf86594d65e914d1ac43f213f1b7f6 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Sun, 18 Oct 2026 17:47:46 -0600 Subject: [PATCH 2/6] fix(hive): prune script dir from sys.path in hook_shim to prevent stdlib types shadowing in Python 3.10 --- src/isycode/hive/hook_shim.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/isycode/hive/hook_shim.py b/src/isycode/hive/hook_shim.py index 77db727..a6acdd2 100644 --- a/src/isycode/hive/hook_shim.py +++ b/src/isycode/hive/hook_shim.py @@ -13,17 +13,32 @@ """ from __future__ import annotations -import json import os -import socket import sys + +# When executed directly (`python hook_shim.py`), Python puts the script's directory +# (`.../src/isycode/hive`) as sys.path[0]. Because `hive/types.py` exists, standard +# library modules (such as json, re, enum, pathlib) attempting `from types import ...` +# would erroneously load `hive/types.py` instead of the stdlib `types`, causing +# an ImportError. +# We must prune the script directory from sys.path and add `src/` before importing anything else. +_script_dir = os.path.dirname(os.path.abspath(__file__)) +while sys.path and ( + os.path.abspath(sys.path[0]) == _script_dir + or sys.path[0].replace("\\", "/").rstrip("/").endswith("/isycode/hive") +): + sys.path.pop(0) + +_src_dir = os.path.abspath(os.path.join(_script_dir, "..", "..")) +if _src_dir not in sys.path: + sys.path.insert(0, _src_dir) + +import json +import socket import time from pathlib import Path from typing import Any -# Add src to path for imports when run as script -sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - from isycode.hive.manager import HiveManager, HiveConfig from isycode.hive.guard import hive_write_decision, HiveWriteInput From f6a966d1ec1cc2b1d7eca7250150c7245b6e338f Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Sun, 18 Oct 2026 18:31:26 -0600 Subject: [PATCH 3/6] feat(tui): integrate 80x24 responsive proposals and fix Windows hive write guard --- src/isycode/hive/guard.py | 29 +++++++++------ src/isycode/isycode.tcss | 6 ++-- src/isycode/tui.py | 11 +++++- src/isycode/tui_app_rail.py | 57 ++++++++++++++++++++++++++++-- src/isycode/tui_app_sessions.py | 5 +++ src/isycode/tui_screens_harness.py | 11 ++++-- tests/test_visual_tui.py | 7 ++++ 7 files changed, 108 insertions(+), 18 deletions(-) diff --git a/src/isycode/hive/guard.py b/src/isycode/hive/guard.py index e915dac..6448636 100644 --- a/src/isycode/hive/guard.py +++ b/src/isycode/hive/guard.py @@ -74,17 +74,9 @@ def hive_write_decision(i: HiveWriteInput) -> HiveWriteDecision: physical_inside = is_inside(physical_root, physical_target, i["caseInsensitive"]) if not lexical_inside and not physical_inside: return {"deny": False, "reason": None} - if not lexical_inside or not physical_inside or os.path.normcase(os.path.abspath(target)) != os.path.normcase(physical_target): + if not lexical_inside or not physical_inside: return {"deny": True, "reason": "Hive coordination writes cannot traverse symbolic links."} - try: - rel = os.path.relpath(target_n, hive_root_n).replace(os.sep, "/") - except ValueError: - return {"deny": False, "reason": None} - - if is_plumbing(rel, i["agentId"], i["isGod"]): - return {"deny": False, "reason": None} - reason = ( "The hive folder is only for coordination: your own memory.md, inbox and outbox, " "and tasks.json" @@ -92,4 +84,21 @@ def hive_write_decision(i: HiveWriteInput) -> HiveWriteDecision: + ". To reach another agent, write a message into your outbox. " "Save documents and other work in your working folder instead." ) - return {"deny": True, "reason": reason} \ No newline at end of file + + try: + rel = os.path.relpath(target_n, hive_root_n).replace(os.sep, "/") + except ValueError: + return {"deny": False, "reason": None} + + if not is_plumbing(rel, i["agentId"], i["isGod"]): + return {"deny": True, "reason": reason} + + try: + physical_rel = os.path.normpath(norm(os.path.relpath(physical_target, physical_root))).replace(os.sep, "/") + except ValueError: + return {"deny": True, "reason": "Hive coordination writes cannot traverse symbolic links."} + + if not is_plumbing(physical_rel, i["agentId"], i["isGod"]) or physical_rel != norm(rel): + return {"deny": True, "reason": "Hive coordination writes cannot traverse symbolic links."} + + return {"deny": False, "reason": None} \ No newline at end of file diff --git a/src/isycode/isycode.tcss b/src/isycode/isycode.tcss index 4a27f3b..d02ad33 100644 --- a/src/isycode/isycode.tcss +++ b/src/isycode/isycode.tcss @@ -107,12 +107,12 @@ #queue-warning { width: 1fr; height: auto; color: #fbbf24; } #queue-steer-help { width: 5; min-width: 5; height: 1; min-height: 1; border: none; padding: 0; } #idea-box-row { - width: 1fr; height: auto; max-height: 4; + width: 1fr; height: auto; max-height: 3; align-horizontal: center; margin: 0 1; background: transparent; } - #shell-box { display: none; width: 60%; height: auto; max-height: 4; padding: 0 1; border: round #514d5a; background: #1e1f22; } + #shell-box { display: none; width: 60%; height: auto; max-height: 3; padding: 0 1; border: round #514d5a; background: #1e1f22; } #idea-box { - width: 60%; height: auto; max-height: 4; padding: 0 1; + width: 60%; height: auto; max-height: 3; padding: 0 1; border: round #514d5a; border-bottom: none; background: #1e1f22; color: #c7b8d4; } diff --git a/src/isycode/tui.py b/src/isycode/tui.py index 45a0cf8..5d315ea 100644 --- a/src/isycode/tui.py +++ b/src/isycode/tui.py @@ -602,7 +602,7 @@ def _apply_locale_chrome(self) -> None: continue try: self.query_one(PromptArea).placeholder = tr(PromptArea.DEFAULT_PLACEHOLDER) - self.query_one("#idea-box").border_title = tr("Space expand / collapse · double-click copy") + self.query_one("#idea-box").border_title = tr("Idea · Space expand") except (NoMatches, NoScreen): pass try: @@ -990,6 +990,9 @@ def action_escape_to_chat(self) -> None: return self._close_menu() return + if getattr(self, "_rail_visible", False) and getattr(self, "size", None) and self.size.width < 100: + self.action_toggle_sidebar() + return if self._review_request_task and not self._review_request_task.done(): self._review_request_task.cancel() self._set_activity("Stopping external review…", YELLOW) @@ -1867,6 +1870,12 @@ def _paint_idle(self) -> None: return if not board.is_mounted or board.has_class("startup-archived"): return + if getattr(self, "size", None) and self.size.height <= 28: + board.styles.height = 2 + board.styles.padding = (0, 0) + else: + board.styles.height = "auto" + board.styles.padding = (0, 1, 1, 1) width = board.content_size.width or self._idle_content_width() rows = self._landscape_row_budget() board._painted_width = width diff --git a/src/isycode/tui_app_rail.py b/src/isycode/tui_app_rail.py index 90267bf..d1fac87 100644 --- a/src/isycode/tui_app_rail.py +++ b/src/isycode/tui_app_rail.py @@ -60,6 +60,17 @@ class RailMixin: def _apply_rail_width(self, terminal_width: int) -> None: """Keep the rail inside the configured range and available columns.""" + if terminal_width < 100 and getattr(self, "_dedicated_space", False) and getattr(self, "_rail_visible", False): + try: + self.query_one("#main").display = False + self.query_one(SidePanel).styles.width = "100%" + return + except Exception: + pass + try: + self.query_one("#main").display = True + except Exception: + pass available = max(22, terminal_width - 52) preferred = self._rail_width if terminal_width >= 100 else self._rail_compact_width requested = min(preferred, available) @@ -631,10 +642,36 @@ def action_toggle_sidebar(self) -> None: self._rail_visible = not self._rail_visible self._rail_visibility_override = self._rail_visible self._rail_auto_hidden = False - self.query_one(SidePanel).display = self._rail_visible + side_panel = self.query_one(SidePanel) + side_panel.display = self._rail_visible + if self.size.width < 100 and getattr(self, "_dedicated_space", False): + self.query_one("#main").display = not self._rail_visible + if self._rail_visible: + side_panel.styles.width = "100%" + else: + self.query_one("#main").display = True + self._apply_rail_width(self.size.width) self._update_model_button_visibility() self.call_after_refresh(self.query_one(Banner).set_compact, True) + def action_toggle_dedicated_space(self) -> None: + """Dedicated full-width space view in compact terminals.""" + self._dedicated_space = not getattr(self, "_dedicated_space", False) + if self._dedicated_space: + self._rail_visible = True + self._rail_visibility_override = True + self._rail_auto_hidden = False + self.query_one(SidePanel).display = True + if self.size.width < 100: + self.query_one("#main").display = False + self.query_one(SidePanel).styles.width = "100%" + else: + self._apply_rail_width(self.size.width) + else: + self.query_one("#main").display = True + self._apply_rail_width(self.size.width) + self.query_one(SidePanel).display = self._rail_visible + def action_focus_files(self) -> None: self._set_rail_view("files") self.query_one("#workspace-tree", Tree).focus() @@ -647,7 +684,14 @@ def _set_rail_view(self, view: str) -> None: self._rail_visible = True self._rail_visibility_override = True self._rail_auto_hidden = False - self.query_one(SidePanel).display = True + side_panel = self.query_one(SidePanel) + side_panel.display = True + if self.size.width < 100 and getattr(self, "_dedicated_space", False): + self.query_one("#main").display = False + side_panel.styles.width = "100%" + else: + self.query_one("#main").display = True + self._apply_rail_width(self.size.width) self.call_after_refresh(self.query_one(Banner).set_compact, True) self.call_after_refresh(self._update_model_button_visibility) show_files = view == "files" @@ -703,6 +747,15 @@ def _landscape_row_budget(self) -> int: def _idle_board_text(self, measured_width: int | None = None, max_rows: int | None = None) -> Text: """Cell-accurate landscape, model, and integration columns.""" width = measured_width or self._idle_content_width() + height = getattr(self, "size", None) and self.size.height or 40 + if height <= 28: + body = Text() + checking = not self._model_line + model = self._model_line or tr("Checking the configured model") + mode = self._workspace_mode() if callable(getattr(self, "_workspace_mode", None)) else getattr(self, "_workspace_mode", "Classic") + body.append(f"ISYCODE · {model} · {mode}\n", style=MUTED if checking else TEXT) + body.append(tr("Conexiones y herramientas (LSPs, MCPs, Skills) en Espacio"), style=MUTED) + return body body = Text() body.append(render_landscape(width, max_rows)) body.append("\n") diff --git a/src/isycode/tui_app_sessions.py b/src/isycode/tui_app_sessions.py index f410a74..e3cf52b 100644 --- a/src/isycode/tui_app_sessions.py +++ b/src/isycode/tui_app_sessions.py @@ -313,6 +313,11 @@ def _paint_idea_box(self) -> None: except NoMatches: return body = self._idea_box or tr("Capture an idea · Ctrl+Shift+Enter") + is_narrow = getattr(self, "size", None) and self.size.width < 100 + box.border_title = tr("Idea · Space") if is_narrow else tr("Idea · Space expand") + if is_narrow: + box.update(Text(body, style=TEXT)) + return busy = self._loop_task is not None and not self._loop_task.done() label = tr("Idea box") + (" · " + tr("Thinking…") if busy else "") heading = Text(label, style=f"bold {TEXT}") diff --git a/src/isycode/tui_screens_harness.py b/src/isycode/tui_screens_harness.py index ace32d9..20e3445 100644 --- a/src/isycode/tui_screens_harness.py +++ b/src/isycode/tui_screens_harness.py @@ -173,9 +173,16 @@ class ModelsScreen(ModalScreen): #models-scroll { height: 1fr; min-height: 4; width: 1fr; } #model-detail-scroll { width: 46; height: 1fr; margin-left: 1; padding: 0 1; border-left: solid #3f4d59; } #model-detail { height: auto; } + ModelsScreen.-narrow #models-card { width: 100%; max-width: 100%; } ModelsScreen.-narrow #models-body { layout: vertical; } - ModelsScreen.-narrow #model-detail-scroll { width: 100%; height: 6; margin-left: 0; border-left: none; border-top: solid #3f4d59; } - ModelsScreen.-short #model-detail-scroll { height: 4; } + ModelsScreen.-narrow #model-detail-scroll { width: 100%; height: 5; margin-left: 0; border-left: none; border-top: solid #3f4d59; } + ModelsScreen.-short #models-card { height: 100%; max-height: 100%; } + ModelsScreen.-short #models-search { height: 3; margin-bottom: 0; } + ModelsScreen.-short #models-actions { height: 1; } + ModelsScreen.-short #models-actions Button { height: 1; min-height: 1; border: none; padding: 0 1; } + ModelsScreen.-short #model-detail-scroll { height: 3; min-height: 3; } + ModelsScreen.-short #models-scroll { min-height: 7; } + ModelsScreen.-short #models-status { max-height: 1; } .model-group { height: auto; background: transparent; } .model-group > Contents { padding: 0 1; } .model-family-block { height: auto; margin-top: 1; background: transparent; } diff --git a/tests/test_visual_tui.py b/tests/test_visual_tui.py index 2e10c38..2e8caa9 100644 --- a/tests/test_visual_tui.py +++ b/tests/test_visual_tui.py @@ -339,6 +339,13 @@ async def scenario(): for width, height in [(200, 60), (120, 40), (80, 24), (200, 60)]: await pilot.resize_terminal(width, height) board = app.query_one('#idle-board') + if height <= 28: + composer = app.query_one('#composer') + assert composer.region.bottom <= height + board_lines = plain_text(board).splitlines() + assert len(board_lines) <= 2 + assert "ISYCODE" in plain_text(board) + continue # Resize dispatch and the app's post-layout repaint are separate # refreshes on some Textual/platform combinations. Assert only # after the current layout has a matching panorama. From 394f6b070bf9fa7492e22f6d79529d0ef1890258 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Sun, 18 Oct 2026 18:35:52 -0600 Subject: [PATCH 4/6] fix(hive): enforce hook server blocking denial response and security violation audit logging --- src/isycode/hive/hook_server.py | 34 ++++++++++++++++++++++----------- src/isycode/hive/hook_shim.py | 22 ++++++++++++++------- tests/test_hive_regressions.py | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/isycode/hive/hook_server.py b/src/isycode/hive/hook_server.py index a56cdbb..964b4b1 100644 --- a/src/isycode/hive/hook_server.py +++ b/src/isycode/hive/hook_server.py @@ -15,7 +15,7 @@ from .manager import HiveManager, HiveConfig from .guard import hive_write_decision, HiveWriteInput -from .types import HiveMessage, MessageAct +from .types import HiveMessage, MessageAct, RegistryAgent class HookServer: @@ -84,31 +84,39 @@ async def _handle_client( payload = json.loads(line.decode("utf-8")) except json.JSONDecodeError: continue - await self._dispatch(payload) + resp = await self._dispatch(payload) + if resp is not None: + writer.write((json.dumps(resp, ensure_ascii=False) + "\n").encode("utf-8")) + await writer.drain() except Exception: pass finally: writer.close() await writer.wait_closed() - async def _dispatch(self, payload: dict[str, Any]) -> None: + async def _dispatch(self, payload: dict[str, Any]) -> dict[str, Any] | None: """Dispatch hook event to appropriate handler.""" evt_type = payload.get("type") agent_id = payload.get("agent_id") if not agent_id or agent_id not in self.manager.registry.agents: - return + return {"decision": "deny", "reason": "Unknown or unregistered agent"} agent = self.manager.registry.agents[agent_id] + if agent.archived: + return {"decision": "deny", "reason": "Archived Hive agent"} if evt_type == "pre_tool_use": - await self._handle_pre_tool_use(payload, agent) + return await self._handle_pre_tool_use(payload, agent) elif evt_type == "post_tool_use": await self._handle_post_tool_use(payload, agent) + return {"decision": "allow"} elif evt_type == "stop": await self._handle_stop(payload, agent) + return {"decision": "allow"} + return None - async def _handle_pre_tool_use(self, payload: dict[str, Any], agent: RegistryAgent) -> None: + async def _handle_pre_tool_use(self, payload: dict[str, Any], agent: RegistryAgent) -> dict[str, Any]: """PreToolUse: check write guard, update agent status.""" tool_name = payload.get("tool_name", "") tool_input = payload.get("tool_input", {}) @@ -127,12 +135,15 @@ async def _handle_pre_tool_use(self, payload: dict[str, Any], agent: RegistryAge } decision = hive_write_decision(inp) if decision["deny"]: - # Send denial back via a response channel (not implemented yet) + self.manager._append_log({ + "kind": "security_violation", + "agentId": agent.id, + "tool": tool_name, + "reason": decision["reason"], + "ts": time.time(), + }) print(f"[hive] DENY {agent.id} {tool_name}: {decision['reason']}") - # For now, we can't easily send back to the hook; the guard - # in isyco-worlds works because the hook reads the decision - # from the shim's stdout. Our shim does that. - return + return {"decision": "deny", "reason": decision["reason"]} # Update status to working self.manager.set_agent_status(agent.id, "working") @@ -142,6 +153,7 @@ async def _handle_pre_tool_use(self, payload: dict[str, Any], agent: RegistryAge "tool": tool_name, "ts": time.time(), }) + return {"decision": "allow"} async def _handle_post_tool_use(self, payload: dict[str, Any], agent: RegistryAgent) -> None: """PostToolUse: update status, log.""" diff --git a/src/isycode/hive/hook_shim.py b/src/isycode/hive/hook_shim.py index a6acdd2..3dd8b88 100644 --- a/src/isycode/hive/hook_shim.py +++ b/src/isycode/hive/hook_shim.py @@ -71,17 +71,21 @@ def connect_sock(sock_path: str) -> socket.socket | None: return None -def send_to_hive(sock_path: str, payload: dict[str, Any]) -> bool: - """Send a payload to the hive socket. Fire-and-forget.""" +def send_to_hive(sock_path: str, payload: dict[str, Any]) -> dict[str, Any] | None: + """Send a payload to the hive socket and read optional server response.""" sock = connect_sock(sock_path) if not sock: - return False + return None try: data = (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8") sock.sendall(data) - return True + sock.settimeout(0.5) + resp_data = sock.recv(4096) + if resp_data: + return json.loads(resp_data.decode("utf-8").strip()) + return None except Exception: - return False + return None finally: try: sock.close() @@ -113,10 +117,10 @@ def handle_pre_tool_use(data: dict[str, Any], hive_root: Path, agent_id: str) -> if decision["deny"]: return {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": decision["reason"]}} - # Forward to hive for live status + # Forward to hive for live status and server-side verification sock_path = os.environ.get("HIVE_SOCK") if sock_path: - send_to_hive(sock_path, { + server_resp = send_to_hive(sock_path, { "type": "pre_tool_use", "agent_id": agent_id, "tool_name": tool_name, @@ -124,6 +128,10 @@ def handle_pre_tool_use(data: dict[str, Any], hive_root: Path, agent_id: str) -> "cwd": cwd, "ts": time.time(), }) + if server_resp and server_resp.get("decision") == "deny": + return {"hookSpecificOutput": {"hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": server_resp.get("reason", "Denied by Hive server")}} return {} diff --git a/tests/test_hive_regressions.py b/tests/test_hive_regressions.py index d0429a3..f4981bb 100644 --- a/tests/test_hive_regressions.py +++ b/tests/test_hive_regressions.py @@ -199,3 +199,37 @@ def test_malformed_registry_denies_instead_of_crashing_hook(tmp_path, registry): response = handle_pre_tool_use({'tool_name': 'Write', 'tool_input': { 'file_path': str(root / 'board.md')}, 'cwd': str(tmp_path)}, root, 'alice') assert response['hookSpecificOutput']['permissionDecision'] == 'deny' + + +@pytest.mark.asyncio +async def test_hook_server_denies_and_logs_security_violation(tmp_path): + import os + if os.name == 'nt': + pytest.skip('Unix domain socket tests skip on Windows') + from isycode.hive.hook_server import HookServer + from isycode.hive.hook_shim import send_to_hive + manager = hive(tmp_path) + server = HookServer(manager) + await server.start() + try: + sock_path = manager.sock_path() + assert sock_path and os.path.exists(sock_path) + payload = { + 'type': 'pre_tool_use', + 'agent_id': 'alice', + 'tool_name': 'Write', + 'tool_input': {'file_path': str(manager.agent_dir('bob') / 'memory.md')}, + 'cwd': str(tmp_path), + } + resp = await asyncio.to_thread(send_to_hive, sock_path, payload) + assert resp is not None + assert resp.get('decision') == 'deny' + assert 'Hive' in resp.get('reason', '') or 'coordination' in resp.get('reason', '') + finally: + await server.stop() + + log_entries = [json.loads(line) for line in manager.log_path().read_text().splitlines() if line] + violations = [e for e in log_entries if e.get('kind') == 'security_violation'] + assert len(violations) == 1 + assert violations[0]['agentId'] == 'alice' + assert violations[0]['tool'] == 'Write' From 20919cfeb544d69601dde8642c823a73a29f82ff Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Sun, 18 Oct 2026 19:00:52 -0600 Subject: [PATCH 5/6] fix(tui): preserve thinking state in narrow idea box and synchronize m15 authority coverage snapshot --- docs/security/m15-authority-coverage.json | 4 ++-- src/isycode/tui_app_sessions.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/security/m15-authority-coverage.json b/docs/security/m15-authority-coverage.json index f51ebe4..b48f9b0 100644 --- a/docs/security/m15-authority-coverage.json +++ b/docs/security/m15-authority-coverage.json @@ -2167,14 +2167,14 @@ }, { "file": "isycode/tui_app_sessions.py", - "line": 977, + "line": 987, "callable": "isycode.tui_app_sessions.SessionMixin._delete_chat_session", "action": "session.delete", "owner": "session_delete" }, { "file": "isycode/tui_app_sessions.py", - "line": 1017, + "line": 1027, "callable": "isycode.tui_app_sessions.SessionMixin._delete_iteration_session", "action": "session.delete", "owner": "session_delete" diff --git a/src/isycode/tui_app_sessions.py b/src/isycode/tui_app_sessions.py index e3cf52b..76e99f9 100644 --- a/src/isycode/tui_app_sessions.py +++ b/src/isycode/tui_app_sessions.py @@ -315,12 +315,17 @@ def _paint_idea_box(self) -> None: body = self._idea_box or tr("Capture an idea · Ctrl+Shift+Enter") is_narrow = getattr(self, "size", None) and self.size.width < 100 box.border_title = tr("Idea · Space") if is_narrow else tr("Idea · Space expand") - if is_narrow: - box.update(Text(body, style=TEXT)) - return busy = self._loop_task is not None and not self._loop_task.done() label = tr("Idea box") + (" · " + tr("Thinking…") if busy else "") heading = Text(label, style=f"bold {TEXT}") + if is_narrow: + if busy: + heading.append("\n") + heading.append(body, style=TEXT) + box.update(heading) + else: + box.update(Text(body, style=TEXT)) + return try: name = selected_provider_name() model = resolved_chat_model(name) From 7de44997258fcab79354aa66093c958f41b48671 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Sun, 18 Oct 2026 19:28:25 -0600 Subject: [PATCH 6/6] feat(hive): activate contained ISyCode agent conversation on message delivery --- src/isycode/hive/manager.py | 8 +++- src/isycode/hive/router.py | 17 ++++++++- src/isycode/tui_app_sessions.py | 29 ++++++++++++++ tests/test_hive_regressions.py | 67 ++++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 5 deletions(-) diff --git a/src/isycode/hive/manager.py b/src/isycode/hive/manager.py index b61b355..6471fa6 100644 --- a/src/isycode/hive/manager.py +++ b/src/isycode/hive/manager.py @@ -279,11 +279,15 @@ def archive_agent(self, agent_id: str) -> None: # ─── Router ─── - async def start_router(self) -> None: + async def start_router(self, on_message_delivered: Callable[[str, HiveMessage], Any] | None = None) -> None: + if on_message_delivered is not None: + self._on_message_delivered = on_message_delivered + cb = getattr(self, "_on_message_delivered", None) if self._router is None: - self._router = Router(self.hive_root, lambda: self._registry) + self._router = Router(self.hive_root, lambda: self._registry, on_message_delivered=cb) self._router_task = asyncio.create_task(self._router.start()) elif self._router_task is None or self._router_task.done(): + self._router.on_message_delivered = cb self._router_task = asyncio.create_task(self._router.start()) async def stop_router(self) -> None: diff --git a/src/isycode/hive/router.py b/src/isycode/hive/router.py index d691a59..ba39745 100644 --- a/src/isycode/hive/router.py +++ b/src/isycode/hive/router.py @@ -23,9 +23,15 @@ class Router: HOP_CAP = 12 SCAN_INTERVAL = 1.0 # seconds - def __init__(self, hive_root: Path, registry: Registry | Callable[[], Registry]) -> None: + def __init__( + self, + hive_root: Path, + registry: Registry | Callable[[], Registry], + on_message_delivered: Callable[[str, HiveMessage], Any] | None = None, + ) -> None: self.hive_root = hive_root self._registry_source = registry + self.on_message_delivered = on_message_delivered self._task: asyncio.Task | None = None self._running = False self._last_route_at = 0.0 @@ -148,7 +154,14 @@ async def _deliver_message(self, from_agent_id: str, msg_file: Path) -> None: ) dest = inbox / f"{msg.id}.json" if not (inbox / ".done" / dest.name).exists(): - create_message(self.hive_root, dest, delivered.to_jsonl() + "\n") + if create_message(self.hive_root, dest, delivered.to_jsonl() + "\n"): + if self.on_message_delivered: + try: + res = self.on_message_delivered(to_agent_id, delivered) + if asyncio.iscoroutine(res): + await res + except Exception: + pass # Archive from sender's outbox self._archive_sent(from_agent_id, msg_file, "delivered") diff --git a/src/isycode/tui_app_sessions.py b/src/isycode/tui_app_sessions.py index 76e99f9..d15ab7a 100644 --- a/src/isycode/tui_app_sessions.py +++ b/src/isycode/tui_app_sessions.py @@ -1545,6 +1545,35 @@ async def _deliver_session_message(self, raw: str) -> None: if getattr(self, "is_running", False): self.run_worker(self._refresh_work_list(), group="work-list") + async def _activate_hive_agent_message(self, agent_id: str, message: HiveMessage) -> None: + """Activate an ISyCode conversation turn for the recipient Hive agent.""" + lane_key = f"hive:{agent_id}" + lane = self._lanes.get(lane_key) + if lane is None: + lane = open_lane(self, lane_key) + + sender = message.from_agent + subject = message.subject or "Notification" + body = message.body or "" + act = message.act.value if hasattr(message.act, "value") else str(message.act) + + prompt = ( + f"[Hive message from @{sender} · {act.upper()} · {subject}]\n\n" + f"{body}\n\n" + f"(You are responding as Hive agent @{agent_id}. Coordinate by writing responses " + f"to your outbox at agents/{agent_id}/outbox/ or updating your memory at agents/{agent_id}/memory.md)." + ) + + status = self._post_user_text(lane, prompt) + if getattr(self, "is_running", False): + self.run_worker(self._refresh_work_list(), group="work-list") + if status == "started": + self._append(f" [Hive] Dispatched message to agent @{agent_id} in conversation lane {lane_key}.", CYAN) + elif status == "queued": + self._append(f" [Hive] Queued message for agent @{agent_id} behind its current turn.", YELLOW) + elif status == "full": + self._append(f" [Hive] Agent @{agent_id} lane queue is full; message remains pending in inbox.", YELLOW) + for _attr, _field in LANE_FIELDS: setattr(SessionMixin, _attr, lane_property(_field)) diff --git a/tests/test_hive_regressions.py b/tests/test_hive_regressions.py index f4981bb..a5fa482 100644 --- a/tests/test_hive_regressions.py +++ b/tests/test_hive_regressions.py @@ -5,7 +5,7 @@ import pytest -from isycode.hive import HiveManager, HiveConfig, AgentMeta, MessageAct +from isycode.hive import HiveManager, HiveConfig, AgentMeta, MessageAct, HiveMessage from isycode.hive.router import Router from isycode.hive.spawn import spawn_hive_agent @@ -233,3 +233,68 @@ async def test_hook_server_denies_and_logs_security_violation(tmp_path): assert len(violations) == 1 assert violations[0]['agentId'] == 'alice' assert violations[0]['tool'] == 'Write' + + +@pytest.mark.asyncio +async def test_router_activates_on_message_delivered_callback(tmp_path): + manager = hive(tmp_path) + delivered_events = [] + + def on_delivered(recipient_id: str, msg: HiveMessage): + delivered_events.append((recipient_id, msg.from_agent, msg.subject, msg.body)) + + manager.send_message('alice', 'bob', MessageAct.INFORM, 'task review', 'please inspect the diff') + router = Router(manager.hive_root, manager.registry, on_message_delivered=on_delivered) + await router._route_once() + + assert len(delivered_events) == 1 + recipient, sender, subject, body = delivered_events[0] + assert recipient == 'bob' + assert sender == 'alice' + assert subject == 'task review' + assert body == 'please inspect the diff' + assert len(manager.read_inbox('bob')) == 1 + + +@pytest.mark.asyncio +async def test_session_mixin_activates_contained_hive_agent_message(): + from isycode.tui_app_sessions import SessionMixin + + class FakeLane: + def __init__(self): + self.posted = [] + + class App: + def __init__(self): + self._lanes = {} + self.posted_texts = [] + self.appended = [] + self.is_running = False + + def _post_user_text(self, lane, text): + self.posted_texts.append((lane, text)) + return "started" + + def _append(self, text, color=None): + self.appended.append((text, color)) + + app = App() + lane = FakeLane() + app._lanes["hive:bob"] = lane + msg = HiveMessage( + id="msg-1", + conversation="conv-1", + in_reply_to=None, + from_agent="alice", + to_agent="bob", + act=MessageAct.REQUEST, + subject="review patch", + body="please review", + ) + await SessionMixin._activate_hive_agent_message(app, "bob", msg) + + assert len(app.posted_texts) == 1 + assert app.posted_texts[0][0] is lane + assert "review patch" in app.posted_texts[0][1] + assert "alice" in app.posted_texts[0][1] + assert any("Dispatched message to agent @bob" in m[0] for m in app.appended)