Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .deckent/workspace/IDENTITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Direction (2026-06-29 pivot): Tool-driven, progressive-disclosure, full-control
Moat: Deterministik eval-backed orchestration · governance-by-construction · outcome→evidence→routing→promotion→training-trace kapalı öğrenme döngüsü
SSOT: `docs/MASTER-PLAN.md` · core-memory: `.deckent/docs/core-memory/MEMORY.md` · yön gerekçesi: `.analysis/hermes-vs-deckent-direction-decisions.md`
<!-- AUTOGEN:START id="identity-tests" -->
Tests: 34,179 descriptors (parsed from tests/**/*.test.ts(x))
Tests: 34,190 descriptors (parsed from tests/**/*.test.ts(x))
Dashboard Tests: 96 descriptors (parsed from src/dashboard/src/**/*.test.tsx)
Coverage: N/A
<!-- AUTOGEN:END id="identity-tests" -->
Expand Down
2 changes: 1 addition & 1 deletion .deckent/workspace/stats-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"$comment": "Deliberately-updated volatile stat snapshot (MASTER-PLAN 521, CI-STATS-HERMETIC-001). Single hermetic source for badge inputs not derivable from tracked files. Refresh: node scripts/update-readme-stats.mjs --refresh-snapshot [--with-coverage] --write",
"sprint": 492,
"coverage": null,
"refreshedAt": "2026-08-05T21:28:58.653Z"
"refreshedAt": "2026-08-07T11:06:19.141Z"
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Deckent's three immutable laws are Dual Lens + Scale, Every Environment, and Nev
License: MIT. [Evidence: `package.json:90-91`; `LICENSE`]

<!-- AUTOGEN:START id="badges" -->
[![npm version](https://img.shields.io/npm/v/deckent.svg)](https://www.npmjs.com/package/deckent) [![tests](https://img.shields.io/badge/tests-34179%2B-brightgreen)](https://github.com/VerhexIO/deckent) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![sprints](https://img.shields.io/badge/sprints-492%2B-teal)](https://github.com/VerhexIO/deckent) [![version](https://img.shields.io/badge/version-v1.0.0--beta.1-orange)](https://github.com/VerhexIO/deckent) [![CI](https://img.shields.io/github/actions/workflow/status/VerhexIO/deckent/ci.yml?label=ci)](https://github.com/VerhexIO/deckent/actions)
[![npm version](https://img.shields.io/npm/v/deckent.svg)](https://www.npmjs.com/package/deckent) [![tests](https://img.shields.io/badge/tests-34190%2B-brightgreen)](https://github.com/VerhexIO/deckent) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![sprints](https://img.shields.io/badge/sprints-492%2B-teal)](https://github.com/VerhexIO/deckent) [![version](https://img.shields.io/badge/version-v1.0.0--beta.1-orange)](https://github.com/VerhexIO/deckent) [![CI](https://img.shields.io/github/actions/workflow/status/VerhexIO/deckent/ci.yml?label=ci)](https://github.com/VerhexIO/deckent/actions)
<!-- AUTOGEN:END id="badges" -->

<!-- AUTOGEN:START id="stat-counts" -->
Expand Down
2 changes: 1 addition & 1 deletion README.tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Deckent'in üç Immutable Law'u Dual Lens + Scale, Every Environment ve Never MV
License: MIT. [Kanıt: `package.json:90-91`; `LICENSE`]

<!-- AUTOGEN:START id="badges" -->
[![npm version](https://img.shields.io/npm/v/deckent.svg)](https://www.npmjs.com/package/deckent) [![tests](https://img.shields.io/badge/tests-34179%2B-brightgreen)](https://github.com/VerhexIO/deckent) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![sprints](https://img.shields.io/badge/sprints-492%2B-teal)](https://github.com/VerhexIO/deckent) [![version](https://img.shields.io/badge/version-v1.0.0--beta.1-orange)](https://github.com/VerhexIO/deckent) [![CI](https://img.shields.io/github/actions/workflow/status/VerhexIO/deckent/ci.yml?label=ci)](https://github.com/VerhexIO/deckent/actions)
[![npm version](https://img.shields.io/npm/v/deckent.svg)](https://www.npmjs.com/package/deckent) [![tests](https://img.shields.io/badge/tests-34190%2B-brightgreen)](https://github.com/VerhexIO/deckent) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![sprints](https://img.shields.io/badge/sprints-492%2B-teal)](https://github.com/VerhexIO/deckent) [![version](https://img.shields.io/badge/version-v1.0.0--beta.1-orange)](https://github.com/VerhexIO/deckent) [![CI](https://img.shields.io/github/actions/workflow/status/VerhexIO/deckent/ci.yml?label=ci)](https://github.com/VerhexIO/deckent/actions)
<!-- AUTOGEN:END id="badges" -->

<!-- AUTOGEN:START id="stat-counts" -->
Expand Down
8 changes: 7 additions & 1 deletion docs/MASTER-PLAN.md

Large diffs are not rendered by default.

1,917 changes: 1,917 additions & 0 deletions docs/audits/CROSS-VERIFICATION-2026-08-06.md

Large diffs are not rendered by default.

1,383 changes: 1,383 additions & 0 deletions docs/audits/attempt-effect-attribution-authority-design-2026-08-06.md

Large diffs are not rendered by default.

1,035 changes: 1,035 additions & 0 deletions docs/audits/audit-authority-integrity-design-2026-08-06.md

Large diffs are not rendered by default.

1,932 changes: 1,932 additions & 0 deletions docs/audits/content-provenance-context-integrity-authority-design-2026-08-06.md

Large diffs are not rendered by default.

1,567 changes: 1,567 additions & 0 deletions docs/audits/enforcement-module-disposition-authority-design-2026-08-06.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

610 changes: 610 additions & 0 deletions docs/audits/plugin-admission-authority-design-2026-08-05.md

Large diffs are not rendered by default.

1,297 changes: 1,297 additions & 0 deletions docs/audits/project-inventory-scope-admission-authority-design-2026-08-06.md

Large diffs are not rendered by default.

1,200 changes: 1,200 additions & 0 deletions docs/audits/provider-neutral-worker-execution-authority-design-2026-08-06.md

Large diffs are not rendered by default.

765 changes: 765 additions & 0 deletions docs/audits/rolling-spend-budget-authority-design-2026-08-05.md

Large diffs are not rendered by default.

1,212 changes: 1,212 additions & 0 deletions docs/audits/terminal-session-execution-authority-design-2026-08-06.md

Large diffs are not rendered by default.

75 changes: 69 additions & 6 deletions docs/generated/master-plan-active.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
"generatedFrom": "docs/MASTER-PLAN.md",
"sourceDigest": {
"algorithm": "sha256(normalized-lf-utf8)",
"value": "e2af682f03ba8b2fc1828b3a6c46f7e02f1aef49165011e4f884e1bf7e659815"
"value": "9059986c2b3dbe31f1ffe7e38ebfc40c1b917fddbb122da0e0e78ced560f46f3"
},
"summary": {
"total": 388,
"active": 341,
"terminal": 47,
"receipts": 109,
"receipts": 115,
"byState": {
"OPEN": 255,
"READY": 0,
Expand Down Expand Up @@ -3152,7 +3152,7 @@
"state": "VERIFY",
"updated": "2026-08-07",
"definitionDigest": "7ade16473831b4aeaf6ba4c3c2e23e0278504413a8cbccad5832065e28c1673d",
"progressDigest": "3f4007ecc2f702ee3348cee1aa6214897514b398915484dca5e98dd29b3483c2",
"progressDigest": "ffc3575442099032ac8ec060a8eb94865fd708464423d2c9c0ee7dc334ce8ae6",
"terminalClosureDigest": null
},
{
Expand Down Expand Up @@ -5423,6 +5423,66 @@
"transitionAt": "2026-08-07T10:53:44Z"
},
"g7AttemptIdentity": null
},
{
"id": "GR-2026-08-07-TENANT-T3-01",
"authorityDigest": "688bc1c6e5bff569cf79c56fb5889201caed95548c1c96de11809807ee61d77d",
"lifecycle": {
"mode": "ONE_SHOT",
"status": "consumed",
"transitionAt": "2026-08-07T11:00:57Z"
},
"g7AttemptIdentity": null
},
{
"id": "GR-2026-08-07-T3-SUPP-01",
"authorityDigest": "20e8e927c325db4be7b6f4a5d4bfcfde0f6b38e9da48af6ea4dd509dd37b36d4",
"lifecycle": {
"mode": "ONE_SHOT",
"status": "consumed",
"transitionAt": "2026-08-07T11:06:19Z"
},
"g7AttemptIdentity": null
},
{
"id": "GR-2026-08-07-TENANT-T4A-01",
"authorityDigest": "9d0688dec6fdb9d285b6136ad2c6761c794a87f2f61b2d38d03b3520d91b922b",
"lifecycle": {
"mode": "ONE_SHOT",
"status": "consumed",
"transitionAt": "2026-08-07T11:32:34Z"
},
"g7AttemptIdentity": null
},
{
"id": "GR-2026-08-07-T4A-SUPP-01",
"authorityDigest": "a04ccb2ba2c9afd0e73e345886a14f08892649dd61114b894a3ec4a4b0d739d5",
"lifecycle": {
"mode": "ONE_SHOT",
"status": "consumed",
"transitionAt": "2026-08-07T11:32:34Z"
},
"g7AttemptIdentity": null
},
{
"id": "GR-2026-08-07-T4A-SUPP-02",
"authorityDigest": "a8f9333f8df9c36e13a4469ba8b1089cbbd125d3aa77eaa175ee929c2cfbb5a6",
"lifecycle": {
"mode": "ONE_SHOT",
"status": "consumed",
"transitionAt": "2026-08-07T11:37:17Z"
},
"g7AttemptIdentity": null
},
{
"id": "GR-2026-08-07-T4A-SUPP-03",
"authorityDigest": "cff7587734b7b9664c72d177f701aeba101f6d11a4cef057769ecc6af3092e76",
"lifecycle": {
"mode": "ONE_SHOT",
"status": "consumed",
"transitionAt": "2026-08-07T11:38:17Z"
},
"g7AttemptIdentity": null
}
],
"workItems": [
Expand Down Expand Up @@ -15520,10 +15580,13 @@
"closureBlockedBy": [],
"evidenceReceipts": [
"GR-2026-08-07-TENANT-T1-01",
"GR-2026-08-07-TENANT-T2-01"
"GR-2026-08-07-TENANT-T2-01",
"GR-2026-08-07-TENANT-T3-01",
"GR-2026-08-07-TENANT-T4A-01",
"GR-2026-08-07-T4A-SUPP-01"
],
"acceptance": "Read, write, event, memory, run, flow and admin paths share fail-closed scope; IDOR tests",
"evidence": "2026-07-27 code-truth: Flow raw tenant/id'yi path'e katıyor, iki store layout var, registry/scheduler yalnız flow.id ile key ediyor; Mission/WorkItem IDs global ve API strict tenant composition unwired; `receipt=GR-2026-08-07-TENANT-T1-01`; T1 admission 2026-08-07 (Dalga-3 ikinci yolcu); 2026-08-07 T1 SETTLEMENT: strict_tenant_isolation artık gerçekten kapıyor — core'a resolveCallerTenant + typed TenantScopeError, API propose ingress'inde erken kapı (strict AÇIK: tenant-claim'siz çağıran 403 ile reddedilir ve akış oluşmaz; strict KAPALI: local varsayılanı bayt-değişmez); bulgu P1d ile aynı sınıftı — bayrak yalnız compliance-report'ta okunuyordu, hiçbir kararı etkilemiyordu; ayrıca dilim sırasında bir gerçek hata yakalandı ve düzeltildi: red throw'u try bloğunun dışında kalınca istek askıda kalıyordu (20s timeout), 403 erken yanıta çevrildi; `proof=tenant-strict-mode-3pins-api-106files-1036-green`; kalan kapsam (memory/run/event/admin yolları + IDOR matrisinin tamamı) T2 successor'ıdır; `receipt=GR-2026-08-07-TENANT-T2-01`; T2 admission 2026-08-07; 2026-08-07 T2 SETTLEMENT: tenant-scope kararı missions (liste+tekil) ve autonomous (chain okuma+mutation) ingress'lerine yayıldı — dört callsite tek karara bağlandı; yeni src/api/tenant-scope.ts fail-soft sync bayrak okuyucu + resolveApiCallerTenant (core resolveCallerTenant'ını kullanır; core config-loader-free kalır — P1b kontratı, route'lar senkron olduğundan okuma API katmanında); strict AÇIK tenant-claim'siz çağıranı 403 ile reddeder, strict KAPALI v1 bayt-değişmez, bozuk config sessizce sertleştirmez; `proof=tenant-t2-idor-pins-api-105files-1025-green`; 3 yeni IDOR pini + api sınıfı yeşil; kalan kapsam (memory/run/event/admin CLI+MCP yüzeyleri ve tam IDOR matrisi) T3 successor'ıdır",
"evidence": "2026-07-27 code-truth: Flow raw tenant/id'yi path'e katıyor, iki store layout var, registry/scheduler yalnız flow.id ile key ediyor; Mission/WorkItem IDs global ve API strict tenant composition unwired; `receipt=GR-2026-08-07-TENANT-T1-01`; T1 admission 2026-08-07 (Dalga-3 ikinci yolcu); 2026-08-07 T1 SETTLEMENT: strict_tenant_isolation artık gerçekten kapıyor — core'a resolveCallerTenant + typed TenantScopeError, API propose ingress'inde erken kapı (strict AÇIK: tenant-claim'siz çağıran 403 ile reddedilir ve akış oluşmaz; strict KAPALI: local varsayılanı bayt-değişmez); bulgu P1d ile aynı sınıftı — bayrak yalnız compliance-report'ta okunuyordu, hiçbir kararı etkilemiyordu; ayrıca dilim sırasında bir gerçek hata yakalandı ve düzeltildi: red throw'u try bloğunun dışında kalınca istek askıda kalıyordu (20s timeout), 403 erken yanıta çevrildi; `proof=tenant-strict-mode-3pins-api-106files-1036-green`; kalan kapsam (memory/run/event/admin yolları + IDOR matrisinin tamamı) T2 successor'ıdır; `receipt=GR-2026-08-07-TENANT-T2-01`; T2 admission 2026-08-07; 2026-08-07 T2 SETTLEMENT: tenant-scope kararı missions (liste+tekil) ve autonomous (chain okuma+mutation) ingress'lerine yayıldı — dört callsite tek karara bağlandı; yeni src/api/tenant-scope.ts fail-soft sync bayrak okuyucu + resolveApiCallerTenant (core resolveCallerTenant'ını kullanır; core config-loader-free kalır — P1b kontratı, route'lar senkron olduğundan okuma API katmanında); strict AÇIK tenant-claim'siz çağıranı 403 ile reddeder, strict KAPALI v1 bayt-değişmez, bozuk config sessizce sertleştirmez; `proof=tenant-t2-idor-pins-api-105files-1025-green`; 3 yeni IDOR pini + api sınıfı yeşil; kalan kapsam (memory/run/event/admin CLI+MCP yüzeyleri ve tam IDOR matrisi) T3 successor'ıdır; `receipt=GR-2026-08-07-TENANT-T3-01`; T3 admission 2026-08-07; 2026-08-07 T3 SETTLEMENT: kalan iki merkezi NULL-tenant sitesi kapatıldı — /api/plan ingress'i ve /api/terminal/* (kabuk erişimi taşıdığından tenant sınırının en duyarlı yüzeyi); ikisi de T2'nin paylaşılan resolveApiCallerTenant kararına bağlandı (strict AÇIK 403, strict KAPALI v1 bayt-değişmez); doğrulama notu: patch'lenen ilk site 410 ile emekli /api/start DEĞİL canlı /api/plan uçudur — ölü koda kablo çekilmediği kontrol edildi; `proof=tenant-t3-resolver-contract-pins-api-green`; 2 yeni resolver-kontrat pini (strict/permissive/tenant'lı + bozuk-config fail-soft), api sınıfı yeşil, tsc temiz; KAPSAM DÜZELTMESİ (T4 ön-kontrolünde bulundu, aynı gün): T3'ün kapattığı `/api/terminal/*` **HTTP rotalarıdır**; asıl kabuk borusu olan WebSocket upgrade (`attachTerminalGateway`, `src/api/terminal/ws-gateway.ts`) HTTP handler'ından GEÇMEZ ve token-only auth'ta `authTenant='local'` ile kabul edilir — yani HTTP'de 403 alan çağıran WS üzerinden kabuk alabilir; ayrıca T2/T3'ün dayandığı senkron `readStrictTenantIsolation` yalnız proje config'ini okur, `loadConfig`'in global katmanını (`resolveGlobalConfigReadPath`) görmez — global'de strict açan operatörde API katmanı sessizce permissive kalır (bu, daha önce iki kez kapatılan 'raporlar-ama-kapatmaz kontrol' sınıfının üçüncü örneği); ikisi de T4a diliminde kapatılacak, T4b kalan ingress (memory-search, process read+write, enterprise), T4c CLI+MCP 0-hardcode host-tenant çözümü; `receipt=GR-2026-08-07-TENANT-T4A-01`; T4a admission 2026-08-07; 2026-08-07 T4a SETTLEMENT: yukarıdaki kapsam-düzeltmesinin iki maddesi de kapatıldı. (1) Efektif bayrak — senkron okuyucu artık `loadConfig`'in katman zincirini yürüyor (defaults→global→project, yakın katman kazanır); bozuk bir katman 'görüş bildirmedi' sayılır, sessizce ne sertleştirir ne gevşetir; global-config yol çözümü saf yol-modülüne indirilip eski yerinden yeniden export edildi, böylece API katmanı toptan mock'lanan ağır config modülünü import etmiyor (davranış ve mevcut importer'lar aynı). Bu düzeltme T2/T3'ün bağladığı altı siteyi de gerçekten kapatır. (2) WS kabuk borusu — strict modda tenant'ı çözülemeyen çağıran upgrade'de typed 4403 ile reddedilir (HTTP 403'ün WS karşılığı) ve `bridge()`'e hiç ulaşılmaz; strict kapalıyken her iki yol da bayt-değişmez. Tier-1 kanıtı gerçek-sunucu koşusudur: `createHttpServer` + gerçek PTY backend + gerçek ws istemcisi, mock yok — strict AÇIK 4403 kapanışı, strict KAPALI açık kalan soket. Harness'ın ilk sürümü yanlış negatif verdi (gateway el-sıkışmayı tamamlayıp sonra kapatıyor, mevcut 4401 yolu da öyle); prob 'open' ile 'sunucu kapattı mı' ayrımına çevrildi. `proof=tenant-t4a-real-server-ws-4403-plus-layer-chain-pins`; 4 birim + 3 katman-öncelik + 2 gerçek-sunucu pini, api+core 622 dosya / 10176 test yeşil, tsc temiz, lint:gates tam zincir yeşil. KANIT SIKILAŞTIRMA (aynı gün, supp-02): gerçek-sunucu pini yalnız '4403 ile kapatıldı'yı doğruluyordu; el-sıkışma tamamlandığından bu, soketin kısa süre köprülenip sonra kapandığı bir dünyayla da uyumluydu — kabuk borusunda 'geç reddetme' ile 'hiç köprülememe' farklı güvenlik özellikleridir. Pin artık reddedilen upgrade'den sonra HİÇ oturum yaratılmadığını ve el-sıkışma biter bitmez gönderilen create çerçevesine HİÇ yanıt dönmediğini de doğruluyor — yeşil, yani reddetme geç değil. AÇIK BIRAKILAN (typed): paketlenmiş-binary (`deckent serve`) teyidi alınmadı çünkü build adımı host guard'ı tarafından reddedildi (atlanmadı — bloklandı; owner'ın build yetkilendirmesi bu maddeyi kapatır). Kanıt kaynak-üstü gerçek sunucudur, binary iddiası yoktur. T4b ŞİDDET NOTU (ölçümle doğrulandı): memory-search'te iddiasız çağıran için tenant yüklemi hiç kurulmuyor — sorgu etiketsiz havuzu değil TÜM kiracıları döndürür, yani T4b'nin en geniş sızıntısı odur ve önce o kapatılır. Kalan kapsam: T4b (memory-search tenant yüklemi hiç kurulmuyor, process read+write, enterprise scope + audit damgaları), T4c (CLI+MCP host-tenant 0-hardcode çözümü); `receipt=GR-2026-08-07-T4A-SUPP-01`",
"updated": "2026-08-07"
},
{
Expand Down Expand Up @@ -19742,6 +19805,6 @@
],
"registryIntegrity": {
"algorithm": "sha256(canonical-json-utf8)",
"value": "4137a09da3ffadb5da5fc44da46647e8bae0a55a6d16dca7ebf79d56e106bd66"
"value": "d6aa2fc19e07823fdacd0520aa07bf312eb42a2b56e82f845f9adaa2cef8c11c"
}
}
2 changes: 1 addition & 1 deletion docs/generated/master-plan-active.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

**Schema:** 3

**Source digest:** `sha256(normalized-lf-utf8):e2af682f03ba8b2fc1828b3a6c46f7e02f1aef49165011e4f884e1bf7e659815`
**Source digest:** `sha256(normalized-lf-utf8):9059986c2b3dbe31f1ffe7e38ebfc40c1b917fddbb122da0e0e78ced560f46f3`

**Rows:** 388 total · 341 active · 47 terminal

Expand Down
6 changes: 3 additions & 3 deletions scripts/lint-test-hermeticity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ export const UNRESOLVED_BASELINE = Object.freeze({
// 2026-08-06 (485a): +4 dashboard overlay tests — same 12480, digest only.
// 2026-08-06 (P1d): +1 config-carry fixture test. Prior: 485a (12480).
// 2026-08-07 (P1e): +2 end-to-end denial pins. Prior: P1d (12481).
count: 12483,
digest: '037c7201b738b4230419f38ea0e6e38619094897b58936d6cf52bb09135bbb84',
count: 12485,
digest: '1b1236e57234706bc52615eb5dbdedbd66c0a353f68b898244f8aca2c56f8bde',
});

export const PRODUCTION_INVENTORY_BASELINE = Object.freeze({
Expand All @@ -140,7 +140,7 @@ export const PRODUCTION_INVENTORY_BASELINE = Object.freeze({
// 2026-08-06 (P1c): CLI plan identity conversion — same 1198, digest only.
// 2026-08-06 (P1d): config carry line + type decls — same 1198, digest only.
count: 1201,
digest: '6455c36ef84990dd8c7309470a9147d95c0485c0c8be7c0daf76ce8d6488cb4b',
digest: '618de52f9f10d42525e7c767e79b22b03a75352255f9aad32cf1bb8b02b2f0bb',
});

const PROTECTED_ROOT_POLICY = new Map([
Expand Down
23 changes: 21 additions & 2 deletions src/api/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http';
import { resolveApiCallerTenant, readStrictTenantIsolation } from './tenant-scope.js';
import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync, renameSync, unlinkSync, chmodSync } from 'node:fs';
import { basename, join, extname, resolve } from 'node:path';
import { platform as osPlatform } from 'node:os';
Expand Down Expand Up @@ -1432,7 +1433,14 @@ async function handleRequest(
return;
}
const principal = deriveRequestPrincipal(req);
const tenantId = principal.tenantId ?? 'local';
// TENANT-001 T3: strict mode refuses a tenant-less caller instead of
// folding it into `local` (the NULL-tenant hole). Default-off keeps v1.
const startTenantScope = resolveApiCallerTenant(principal, projectRoot);
if (startTenantScope.tenant === null) {
sendJson(res, { error: startTenantScope.reason }, 403);
return;
}
const tenantId = startTenantScope.tenant;
const actor = {
id: principal.id,
...(principal.role ? { role: principal.role } : {}),
Expand Down Expand Up @@ -2612,7 +2620,14 @@ export function createHttpServer(
const tok = authHeader.replace(/^Bearer\s+/i, '');
// Derive principal from request bearer (claims read from JWT; unverified before auth gate).
const terminalPrincipal = deriveRequestPrincipal(req);
const terminalTenantId: string = terminalPrincipal.tenantId ?? 'local';
// TENANT-001 T3: the terminal surface carries shell access, so a
// tenant-less caller must not inherit `local` here either.
const terminalTenantScope = resolveApiCallerTenant(terminalPrincipal, projectRoot);
if (terminalTenantScope.tenant === null) {
sendJson(res, { error: terminalTenantScope.reason }, 403);
return;
}
const terminalTenantId: string = terminalTenantScope.tenant;
// Async seam (Sprint 268): prefer verifyAsync when the provider defines
// it (JWKS key resolution) — the handler is already async. Sync-only
// providers (LocalToken) keep the exact previous code path.
Expand Down Expand Up @@ -2742,6 +2757,10 @@ export function createHttpServer(
auth: terminalAuth,
audit: terminalAudit,
limiter: terminalLimiter,
// TENANT-001 T4a: the upgrade listener bypasses the HTTP request handler,
// so the tenant decision has to be carried in here explicitly — otherwise
// the WS shell stays open to callers the HTTP routes already refuse.
strictTenantIsolation: readStrictTenantIsolation(projectRoot),
});
// Idle reaper — sweeps stale non-deckent sessions every 30s.
// unref() so the timer does not keep the event loop alive in tests.
Expand Down
Loading
Loading