Skip to content
Closed
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,181 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-34181%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-34181%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
4 changes: 3 additions & 1 deletion docs/MASTER-PLAN.md

Large diffs are not rendered by default.

33 changes: 27 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": "9c377a6fcf1aae2b1644cad34cd2faf0c46497ff66ac798aeceead5611fda30c"
},
"summary": {
"total": 388,
"active": 341,
"terminal": 47,
"receipts": 109,
"receipts": 111,
"byState": {
"OPEN": 255,
"READY": 0,
Expand Down Expand Up @@ -3152,7 +3152,7 @@
"state": "VERIFY",
"updated": "2026-08-07",
"definitionDigest": "7ade16473831b4aeaf6ba4c3c2e23e0278504413a8cbccad5832065e28c1673d",
"progressDigest": "3f4007ecc2f702ee3348cee1aa6214897514b398915484dca5e98dd29b3483c2",
"progressDigest": "a3cd3c98717b90655cf1369135555c635bbc876f016be888d9d8af6c167769f8",
"terminalClosureDigest": null
},
{
Expand Down Expand Up @@ -5423,6 +5423,26 @@
"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
}
],
"workItems": [
Expand Down Expand Up @@ -15520,10 +15540,11 @@
"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"
],
"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ü",
"updated": "2026-08-07"
},
{
Expand Down Expand Up @@ -19742,6 +19763,6 @@
],
"registryIntegrity": {
"algorithm": "sha256(canonical-json-utf8)",
"value": "4137a09da3ffadb5da5fc44da46647e8bae0a55a6d16dca7ebf79d56e106bd66"
"value": "7322740089f36a1befcbdf41f6f31d381e8398d869d607400e2c091b458d3da2"
}
}
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):9c377a6fcf1aae2b1644cad34cd2faf0c46497ff66ac798aeceead5611fda30c`

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

Expand Down
4 changes: 2 additions & 2 deletions scripts/lint-test-hermeticity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const UNRESOLVED_BASELINE = Object.freeze({
// 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',
digest: 'b4417ea9b741663547c76c401e5f09b0f437bc0bab581bb8a28d62e49c4ebb57',
});

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: '24360439f5fb6a7161b52fe3c2629fee028e365389ccc611cd67f56ae93f4951',
});

const PROTECTED_ROOT_POLICY = new Map([
Expand Down
19 changes: 17 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 } 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
48 changes: 48 additions & 0 deletions tests/api/run-flow-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,51 @@ describe('TENANT-001 T1 — strict_tenant_isolation', () => {
expect(res.body.proposal?.tenant).toBe('local');
});
});

// ═══ TENANT-001 T3 — tenant scope reaches the plan + terminal ingresses ═════
// The API's own `/api/plan` and `/api/terminal/*` surfaces still folded a
// tenant-less caller into `local`. The terminal one carries shell access, so it
// is the most sensitive tenant boundary in the product. These pins assert the
// SHARED resolver behaves identically wherever it is wired.
describe('TENANT-001 T3 — shared tenant resolver contract', () => {
it('strict ON refuses a tenant-less caller, strict OFF keeps local (resolver level)', async () => {
const { resolveApiCallerTenant, readStrictTenantIsolation } =
await import('../../src/api/tenant-scope.js');
const { mkdtempSync: mk, mkdirSync: md, writeFileSync: wf, rmSync: rm } = await import('node:fs');
const { tmpdir: td } = await import('node:os');
const { join: jn } = await import('node:path');

const strictRoot = mk(jn(td(), 'tenant-strict-'));
const permissiveRoot = mk(jn(td(), 'tenant-permissive-'));
try {
md(jn(strictRoot, '.deckent'), { recursive: true });
wf(jn(strictRoot, '.deckent', 'config.json'), JSON.stringify({ strict_tenant_isolation: true }));

expect(readStrictTenantIsolation(strictRoot)).toBe(true);
expect(readStrictTenantIsolation(permissiveRoot)).toBe(false); // absent config → v1 default

const tenantless = { id: 'api-static' };
expect(resolveApiCallerTenant(tenantless, strictRoot).tenant).toBeNull();
expect(resolveApiCallerTenant(tenantless, permissiveRoot).tenant).toBe('local');
expect(resolveApiCallerTenant({ id: 'alice', tenantId: 'acme' }, strictRoot).tenant).toBe('acme');
} finally {
rm(strictRoot, { recursive: true, force: true });
rm(permissiveRoot, { recursive: true, force: true });
}
});

it('a malformed config fails SOFT to the permissive default (never a silent hard-deny)', async () => {
const { readStrictTenantIsolation } = await import('../../src/api/tenant-scope.js');
const { mkdtempSync: mk, mkdirSync: md, writeFileSync: wf, rmSync: rm } = await import('node:fs');
const { tmpdir: td } = await import('node:os');
const { join: jn } = await import('node:path');
const root = mk(jn(td(), 'tenant-broken-'));
try {
md(jn(root, '.deckent'), { recursive: true });
wf(jn(root, '.deckent', 'config.json'), '{ not json');
expect(readStrictTenantIsolation(root)).toBe(false);
} finally {
rm(root, { recursive: true, force: true });
}
});
});
Loading