Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
439beb4
Prepare Naruon commercial pilot frontend
seonghobae Jul 2, 2026
80b613a
Document Naruon pilot PR handoff
seonghobae Jul 2, 2026
8ad4499
Harden Naruon enterprise sale readiness
seonghobae Jul 2, 2026
a970b78
Test pilot smoke localhost guard
seonghobae Jul 2, 2026
2fe2225
Plan Naruon 20B commercial readiness
seonghobae Jul 2, 2026
ec9e58c
Add full product smoke gate
seonghobae Jul 2, 2026
f46fe9c
Fail closed on governance blockers
seonghobae Jul 2, 2026
4e82792
Add 20B buyer handoff package
seonghobae Jul 2, 2026
5e4eb75
Run full product smoke in CI
seonghobae Jul 2, 2026
0caf2a5
Record 20B local verification evidence
seonghobae Jul 2, 2026
d353a0f
Stabilize full product smoke home assertion
seonghobae Jul 2, 2026
9ad9168
Add responsive product design QA evidence
seonghobae Jul 2, 2026
32a06a6
Add critical full product interaction smoke
seonghobae Jul 2, 2026
9fd8159
Add full product accessibility smoke evidence
seonghobae Jul 2, 2026
5b41f2b
Extend full product smoke to mobile interactions
seonghobae Jul 2, 2026
658a52d
Broaden full product interaction smoke coverage
seonghobae Jul 2, 2026
172c5c1
Cover connector rotation in product smoke
seonghobae Jul 2, 2026
8ceb4ed
Cover mail workflow states in product smoke
seonghobae Jul 2, 2026
9b5d87d
Expand workflow-state full product smoke
seonghobae Jul 2, 2026
a62995f
Cover provider completion states in full smoke
seonghobae Jul 2, 2026
d20e5a6
Prove settings startup persistence in full smoke
seonghobae Jul 2, 2026
0373797
Cover search graph evidence in full smoke
seonghobae Jul 2, 2026
6b454e4
Cover security permission evidence in full smoke
seonghobae Jul 2, 2026
25ebfd1
Cover project source evidence in full smoke
seonghobae Jul 2, 2026
c94e187
Cover settings persistence in full smoke
seonghobae Jul 2, 2026
c074d97
Cover AI Hub run evidence in full smoke
seonghobae Jul 2, 2026
110e9ae
Cover security audit evidence in full smoke
seonghobae Jul 2, 2026
4224b10
Cover source drawer focus trap in full smoke
seonghobae Jul 2, 2026
8d0be95
Cover search graph detail controls
seonghobae Jul 2, 2026
c45a38a
Expand search graph selection evidence
seonghobae Jul 2, 2026
51a77ea
Cover project evidence edit in full smoke
seonghobae Jul 2, 2026
3058651
Document project evidence smoke commit
seonghobae Jul 2, 2026
cf67503
Cover security permission denial in full smoke
seonghobae Jul 2, 2026
d681e4b
Persist security permission intent evidence
seonghobae Jul 2, 2026
1feaa1a
Expand security denial intent variants
seonghobae Jul 2, 2026
c7566ef
Gate 20B KPI ROI claims
seonghobae Jul 2, 2026
0c7ae62
fix(email): restore SNI hostname shim for pinned implicit-TLS SMTP
seonghobae Jul 8, 2026
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
10 changes: 10 additions & 0 deletions .github/workflows/app-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,13 @@ jobs:
run: |
rm -f ~/package-lock.json
cd frontend && pnpm run build

- name: Install Playwright Chromium
run: cd frontend && pnpm exec playwright install --with-deps chromium

- name: Run full product smoke
env:
NEXT_TELEMETRY_DISABLED: "1"
NARUON_FULL_PRODUCT_BASE_URL: "http://127.0.0.1:3001"
NARUON_FULL_PRODUCT_SCREENSHOT_DIR: "/tmp/naruon-full-product-smoke"
run: cd frontend && pnpm run full:smoke
132 changes: 131 additions & 1 deletion backend/api/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Literal

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

Expand All @@ -26,6 +26,20 @@

SourceType = Literal["caldav_source", "carddav_source", "webdav_repository"]
ScopeKind = Literal["organization", "personal"]
PermissionDraftDecision = Literal[
"allow_writeback",
"deny_external_write",
"deny_workspace_write",
"deny_region_export",
"deny_missing_consent",
]
PermissionResourceType = Literal[
"caldav_source",
"carddav_source",
"data_export",
"webdav_repository",
"provider_secret",
]
DecisionReason = Literal[
"allowed",
"organization_denied",
Expand Down Expand Up @@ -98,6 +112,28 @@ class SecurityAccessSurfaceResponse(BaseModel):
policy_order: list[PolicyOrderStep]


class PermissionChangeIntentRequest(BaseModel):
model_config = ConfigDict(extra="forbid")

decision: PermissionDraftDecision
resource_type: PermissionResourceType


class PermissionChangeIntentResponse(BaseModel):
decision: PermissionDraftDecision
resource_type: PermissionResourceType
allowed: bool
reason: DecisionReason
evidence_label: str
audit_event: Literal["security.permission_change_intent"]
provider_write_executed: Literal[False]
denial_result: Literal[
"approval_required_before_external_write",
"provider_denied_by_policy",
]
observed_at: str


def _datetime_to_utc_iso(value: datetime) -> str:
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
Expand Down Expand Up @@ -428,6 +464,47 @@ def _require_authoritative_workspace_scope(auth_context: AuthContext) -> None:
)


def _permission_change_resource(
request: PermissionChangeIntentRequest,
auth_context: AuthContext,
) -> ResourcePolicy:
organization_id = auth_context.organization_id
workspace_id = auth_context.workspace_id
data_region = settings.DATA_REGION
required_consent_scopes: tuple[str, ...] = ()

if request.decision == "deny_external_write":
organization_id = "org-outside-scope"
elif request.decision == "deny_workspace_write":
workspace_id = "workspace-outside-scope"
elif request.decision == "deny_region_export":
data_region = settings.SECONDARY_DATA_REGION
elif request.decision == "deny_missing_consent":
required_consent_scopes = ("provider.write",)

return ResourcePolicy(
owner_id=auth_context.user_id,
organization_id=organization_id,
permitted_roles=("tenant_admin", "organization_admin"),
permitted_group_ids=auth_context.group_ids,
data_region=data_region,
required_consent_scopes=required_consent_scopes,
workspace_id=workspace_id,
)


def _permission_detail_text(
request: PermissionChangeIntentRequest,
reason: str,
) -> str:
return (
f"permission_intent={request.decision}; "
f"resource_type={request.resource_type}; "
f"policy_reason={reason}; "
"provider_write_executed=false"
)


@router.get("/access-surface", response_model=SecurityAccessSurfaceResponse)
async def get_access_surface(
auth_context: AuthContext = Depends(get_auth_context),
Expand Down Expand Up @@ -471,3 +548,56 @@ async def get_access_surface(
external_share_reviews=_share_reviews(sources),
policy_order=_policy_order(),
)


@router.post(
"/permission-change-intent",
response_model=PermissionChangeIntentResponse,
)
async def create_permission_change_intent(
request: PermissionChangeIntentRequest,
auth_context: AuthContext = Depends(get_auth_context),
db: AsyncSession = Depends(get_db),
) -> PermissionChangeIntentResponse:
_require_authoritative_workspace_scope(auth_context)
if not is_admin_role(auth_context.role):
raise HTTPException(
status_code=403,
detail="Admin role is required for security permission changes",
)

decision = evaluate_access(
_access_request(auth_context),
_permission_change_resource(request, auth_context),
)
observed_at = datetime.now(timezone.utc)
audit_event = SecurityAuditEvent(
actor_user_id=auth_context.user_id,
actor_role=auth_context.role,
organization_id=auth_context.organization_id,
workspace_id=auth_context.workspace_id,
event_action="permission_change_intent",
resource_type=request.resource_type,
resource_uid=None,
evidence_source="api.security.permission_change_intent",
detail_text=_permission_detail_text(request, decision.reason),
observed_at=observed_at,
)
db.add(audit_event)
await db.commit()

return PermissionChangeIntentResponse(
decision=request.decision,
resource_type=request.resource_type,
allowed=decision.allowed,
reason=decision.reason,
evidence_label="policy_engine_evidence",
audit_event="security.permission_change_intent",
provider_write_executed=False,
denial_result=(
"approval_required_before_external_write"
if decision.allowed
else "provider_denied_by_policy"
),
observed_at=_datetime_to_utc_iso(observed_at),
)
6 changes: 6 additions & 0 deletions backend/services/email_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,12 @@ class _PinnedImplicitTlsSMTP(aiosmtplib.SMTP):

def __init__(self, *, tls_server_hostname: str, **kwargs):
self._tls_server_hostname = tls_server_hostname
# aiosmtplib validates that a socket-backed implicit-TLS client has a
# hostname (it raises "If using a socket with TLS, hostname is
# required"). The pinned client connects an already-resolved socket and
# supplies SNI via ``tls_server_hostname`` in ``_create_connection``, so
# mirror that value into ``hostname`` to satisfy validation without a
# second DNS lookup.
if (
kwargs.get("sock") is not None
and kwargs.get("use_tls")
Expand Down
63 changes: 63 additions & 0 deletions backend/tests/test_release_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,69 @@ def test_pr_governance_uses_metadata_only_events_without_checkout_or_admin_merge
assert "dismiss" not in combined.lower()


def test_20b_kpi_roi_claim_gate_separates_measurements_from_assumptions() -> None:
kpi_report = read_repo_text(
"docs/superpowers/reports/2026-07-02-naruon-kpi-validation.md"
)
buyer_package = read_repo_text(
"docs/superpowers/reports/2026-07-02-naruon-20b-buyer-package.md"
)
security_questionnaire = read_repo_text(
"docs/superpowers/reports/2026-07-02-naruon-20b-security-questionnaire.md"
)
readiness_plan = read_repo_text(
"docs/superpowers/plans/2026-07-02-naruon-20b-full-product-commercial-readiness.md"
)

assert "### ROI Model And Claim Gate" in kpi_report
assert "estimated_period_value_krw" in kpi_report
for model_input in (
"time_saved_per_user_per_week_hours",
"fully_loaded_hourly_cost_krw",
"weekly_active_users",
"evidence_open_rate",
"decision_to_action_conversion_rate",
"pilot_period_weeks",
"risk_reduction_adjustment",
):
assert model_input in kpi_report

assert "Measured value unavailable in this branch" in kpi_report
assert "must not be presented as a proven value" in kpi_report
assert "Naruon has proven a 20B KRW ROI" in kpi_report
assert "No live ROI number should be claimed" in buyer_package
assert "live measured data" in buyer_package
assert "live measured data is required before ROI claims" in security_questionnaire
assert "- [x] **Step 2: Define ROI model**" in readiness_plan
assert "- [ ] **Step 2: Define ROI model**" not in readiness_plan


def test_20b_buyer_package_rejects_final_procurement_claim_language() -> None:
buyer_package = read_repo_text(
"docs/superpowers/reports/2026-07-02-naruon-20b-buyer-package.md"
)
demo_script = read_repo_text(
"docs/superpowers/reports/2026-07-02-naruon-20b-demo-script.md"
)
telemetry_report = read_repo_text(
"docs/superpowers/reports/2026-07-02-naruon-design-to-code-telemetry-qa.md"
)

assert "Accepted buyer-review language:" in buyer_package
assert "Rejected language:" in buyer_package
assert "## Do Not Say" in demo_script
for rejected_claim in (
"Naruon is public-launch ready.",
"Live ROI has been proven.",
"All provider writes are production-proven.",
):
assert rejected_claim in demo_script

assert "controlled enterprise buyer technical review" in buyer_package
assert "not a final public-launch or contract-close claim" in buyer_package
assert "not a claim that Naruon is ready for public SaaS launch" in telemetry_report


def test_coderabbit_approval_is_decoupled_from_github_checks() -> None:
config = read_repo_text(".coderabbit.yaml")
policy = read_repo_text("docs/development/merge-gate-policy.md")
Expand Down
Loading
Loading