diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7282b641d..d83efcc04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ concurrency: jobs: verify: name: verify - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index 8a155b759..89ed4139b 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -23,7 +23,7 @@ env: jobs: verify_image: name: verify-patch-validator-image - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 180 permissions: contents: read diff --git a/.github/workflows/reviewer-ci.yml b/.github/workflows/reviewer-ci.yml index c999450a2..f5212251a 100644 --- a/.github/workflows/reviewer-ci.yml +++ b/.github/workflows/reviewer-ci.yml @@ -16,7 +16,7 @@ permissions: jobs: reviewer: name: reviewer - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 env: NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE: gcr.io/distroless/java-base-debian13:nonroot diff --git a/CHANGELOG.md b/CHANGELOG.md index 47e3bdb90..11519b54d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## Unreleased +- Noema의 필수 PR 워크플로 `ci`, `reviewer-ci`, `patch-validator-image`를 부동 `ubuntu-latest` 대신 명시적 `ubuntu-24.04` GitHub-hosted runner에 고정하고, 인용 여부와 무관하게 `ubuntu-latest` 회귀를 탐지하는 계약 테스트를 추가해 pre-checkout runner-assignment stall의 repository-owned selector 원인을 제거한다. 중앙 `Security Scan`의 runner/control-plane 권한은 별도 `.github` owner 경계에 유지한다. +- 비공개 취약점 보고 감사가 16 KiB 응답 상한, bounded stream 취소, canonical repository/source identity의 독립 검증, SHA-1/SHA-256 exact revision, symlink·retained-path 보호를 실패-폐쇄로 강제한다. 이 감사 결과는 live private reporting 활성화, notification staffing, 실제 advisory 대응 또는 release/deployment 완료 증거를 대신하지 않는다. - External scheduler evidence audits now retain source authority through final report publication: reports are owner-only, no-follow, exclusive one-shot receipts, so a concurrent rename cannot move the accepted source inode onto the report pathname and have it replaced. Source/report path and inode alias checks, single-link retained-source validation, and Unicode control sanitization remain fail closed. - production runtime credential envelope parsing을 fail-closed로 강화한다. GitHub App PKCS#1 key의 canonical PKCS#8 변환은 유지하되, bare carriage return처럼 비정규 body bytes가 포함된 PKCS#8 PEM은 readiness/import 단계의 암묵적 정규화에 넘기지 않고 즉시 거부해 malformed secret이 ready 상태로 승인되지 않게 한다. - acquisition tracked-byte 인증이 descriptor에서 읽은 bytes를 Git blob framing으로 Node 표준 crypto에서 직접 해시해, 파일마다 `git hash-object` subprocess를 만들던 대형 checkout 병목을 제거한다. exact tree inventory는 Git 2.36 전용 `ls-tree --format` 대신 호환되는 기본 NUL 형식을 사용하며, object ID, SHA-1/SHA-256 저장소, no-follow·descriptor identity·byte limit 실패-폐쇄 계약은 유지한다. dependency-license inventory가 실제로 소비한 `package-lock.json` bytes도 pinned source commit의 Git blob과 직접 대조해 transient file swap을 차단한다. 실패한 audit stage 뒤에도 source를 다시 인증한 다음 원래 child status로 종료하므로 failure evidence가 stale revision으로 남지 않으며, release·publication·deployment evidence producer와 acquisition consumer는 canonical SHA-1/SHA-256 commit identity를 동일하게 지원한다. @@ -84,4 +86,4 @@ - 배포 스모크가 `/health`와 `/exchange`의 no-store/nosniff 보안 헤더 및 `/exchange` 401 Bearer challenge까지 검증하도록 `smoke-readiness.sh`와 회귀 테스트를 보강. - `/exchange` 401 응답에 `WWW-Authenticate: Bearer realm="noema"` challenge를 추가하고 인증 누락은 `invalid_request`, 잘못된 토큰은 `invalid_token`으로 구분. - `x-request-id`/`x-correlation-id` 및 client IP 계열 헤더를 길이/문자 기준으로 제한해 로그 오염과 rate-limit key 폭주를 방지. -- `KRW 2,000,000,000` 매각 가능성 Goal 등록서, buyer due diligence index, library/submodule 경계 판단서를 추가하고 `npm run acquisition:audit`로 ARR/LOI/이전성/saleable evidence를 실패-폐쇄 방식으로 검증. +- `KRW 2,000,000,000` 매각 가능성 Goal 등록서, buyer due diligence index, library/submodule 경계 판단서를 추가하고 `npm run acquisition:audit`로 ARR/LOI/이전성/saleable evidence를 실패-폐쇄 방식으로 검증. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 4f6ceb585..6a939eea0 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,63 @@ # Noema -Noema is a ContextualWisdomLab leaf product: a GitHub App credential broker -and an independent LLM pull-request reviewer. It runs on its own, and a host -calls it through a published HTTP API and a secret-free LLM gateway contract. -That hub-and-leaf call is the supported MSA path — **따로 또 같이** — not a -reason to merge repositories. - -It deploys as a [Cloudflare Worker](https://developers.cloudflare.com/workers/) -(Free tier) with two jobs: - -1. **Token exchange.** GitHub Actions presents an - [OIDC](https://docs.github.com/en/actions/reference/security/oidc) JWT - (audience `cwl-noema-review`). Noema verifies issuer, audience, organization - owner, and the exact trusted central workflow identity, then returns a - repository-scoped GitHub App installation token - (`pull_requests: write`, `contents: read`, `checks: read`). -2. **Review.** The default-branch - [`central-review`](./.github/workflows/central-review.yml) runtime accepts a - `noema-review` dispatch and publishes an App-authored verdict. Untrusted - analysis stays in a separate sandbox; see - [`docs/noema-agent-sandbox-plan.md`](./docs/noema-agent-sandbox-plan.md). - -Every Noema LLM job — production review, hourly-product-development, and -host-side judgments — calls `ContextualWisdomLab/contextual-orchestrator`. -Upstream provider keys stay in the orchestrator credential KV. Noema does not -walk a sequential model list or fall back to a direct provider. - -OIDC, Worker binding, and Durable Object trust rules are recorded in -[Architecture doctoring](./docs/doctoring/architecture-trust-boundaries.md) -and the [ADR index](./docs/adr/README.md). Do not treat those records as a new -paper list. - -## Composition hubs - -Leaf products stay independently deployable. Composition hubs call them as -published dependencies. Do not fold Noema into a hub repo. - -| Hub | Role | How it calls Noema | +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ContextualWisdomLab/noema) + +**Evidence-producing credential and maintenance control plane for governed GitHub automation.** + +Noema gives repository automation narrowly scoped capability without turning model output, CI status, or a long-lived secret into authority. It verifies GitHub Actions OIDC identity, exchanges that identity for repository-scoped GitHub App capability, and keeps credential, review, check, merge, release, deployment, and commercial evidence as distinct trust domains. + +It is built for maintainers and platform teams that need automation to move quickly while remaining exact-revision, least-privilege, and fail-closed. + +## What Noema does + +| Need | Noema responsibility | +| --- | --- | +| Short-lived repository capability | Verify GitHub Actions OIDC and mint repository-scoped GitHub App installation tokens | +| Exact-revision maintenance | Bind privileged actions to current heads, live bases, and freshly re-read repository state | +| Independent review evidence | Support App-authored review verdicts without treating model judgement as merge authority | +| Safe automation | Separate untrusted analysis from credential-bearing execution and refuse stale or ambiguous evidence | +| Operational evidence | Preserve bounded readiness, security, governance, release, and maintenance evidence | +| Ecosystem composition | Integrate with central `.github`, `contextual-orchestrator`, and other products through explicit contracts | + +Noema does **not** own model discovery or provider routing. Those capabilities belong to [`ContextualWisdomLab/contextual-orchestrator`](https://github.com/ContextualWisdomLab/contextual-orchestrator). Noema also does not promote checks, scanners, model output, or documentation into formal approval, merge, deployment, customer, revenue, legal, or transfer authority. + +## Product surfaces + +### Credential exchange + +The Cloudflare Worker exposes three intentionally distinct HTTP surfaces: + +| Method | Path | Meaning | | --- | --- | --- | -| [`naruon`](https://github.com/ContextualWisdomLab/naruon) | Judgments and decisions | First-class consumer of the published orchestrator gateway contract. Naruon wiring is a separate repository pull request. | -| [`gyeot` (곁)](https://github.com/ContextualWisdomLab/gyeot) | On-device wellness composition hub | Call Noema through the HTTP API and/or the same published contract when a host needs token exchange or the LLM gateway. | +| `GET` | `/health` | Process liveness only | +| `GET` / `HEAD` | `/ready` | Runtime readiness without reflecting secrets | +| `POST` | `/exchange` | Exchange an authorized GitHub Actions OIDC bearer for short-lived repository capability | + +The public contract is published as [`openapi.json`](./openapi.json), with narrative details in [`docs/api-spec.md`](./docs/api-spec.md). -The machine-readable LLM contract is -[`contracts/orchestrator-gateway.json`](./contracts/orchestrator-gateway.json). -Narrative: [Orchestrator gateway consumer contract](./docs/orchestrator-gateway-consumer-contract.md). -Print it with `node scripts/verify-orchestrator-gateway.mjs --print-contract`. +### Review and maintenance control -Host LLM settings (never an upstream provider key): +Noema can support review and maintenance workflows that need to distinguish: + +- the exact current source head from historical or predecessor evidence; +- an independently resolved live base from a stale PR snapshot; +- checks, statuses, scanners, model judgement, and formal reviews; +- observation authority from mutation authority; +- source integration from release, deployment, and commercial evidence. + +A blocked lane is not a reason to stall unrelated safe work. The product is explicitly work-conserving while remaining fail-closed on the blocked action itself. + +### Orchestrator gateway contract + +Every model-backed Noema job routes through `ContextualWisdomLab/contextual-orchestrator`. Provider credentials remain in the orchestrator credential boundary; Noema consumes only a dedicated inference token and a published gateway contract. + +The machine-readable contract is [`contracts/orchestrator-gateway.json`](./contracts/orchestrator-gateway.json). + +```bash +node scripts/verify-orchestrator-gateway.mjs --print-contract +``` + +Host-facing gateway configuration: | Name | Meaning | | --- | --- | @@ -54,14 +65,11 @@ Host LLM settings (never an upstream provider key): | `NOEMA_LLM_MODEL` | Routing alias, normally `contextual-orchestrator` | | `NOEMA_LLM_API_KEY` | Dedicated gateway inference token | -`GET /healthz` must return -`{"status":"ok","service":"contextual-orchestrator"}`. Known direct-provider -hosts are rejected. Leftover `NOEMA_FALLBACK_*` settings fail closed. +Direct-provider fallbacks are intentionally rejected. -## Run it alone +## Quick start -Requires Node.js 22+ (CI uses Node 24). This package is private; there is no -published npm library. The Worker and the HTTP contract are the product. +Noema is a private Node.js package; the deployed Worker and its HTTP/evidence contracts are the product. The repository requires Node.js 22 or newer and pins the development runtime/package-manager versions in `package.json`. ```bash npm install @@ -70,50 +78,41 @@ npm run typecheck npm run dev ``` -`npm run dev` starts a local Worker. Provision secrets on the Worker binding -(the KV-equivalent), not `process.env` in `src/`: +`npm run dev` starts a local Cloudflare Worker. + +### Configure Worker secrets + +Provision GitHub App secrets on the Worker binding rather than reading them from application `process.env` code: ```bash wrangler secret put GITHUB_APP_ID wrangler secret put GITHUB_APP_PRIVATE_KEY_PEM -# optional: pin a single installation instead of discovering by repository + +# Optional: pin one installation rather than discovering it by repository. wrangler secret put GITHUB_APP_INSTALLATION_ID ``` -Deploy: +### Deploy ```bash npm run deploy ``` -Set `NOEMA_EXCHANGE_URL` in `ContextualWisdomLab/.github` (or the customer -central workflow) to the deployed `/exchange` URL. Production cutover that -creates organization variables or secrets is a separate operator step; see -[contextual-orchestrator reviewer cutover](./docs/contextual-orchestrator-reviewer-cutover.md). -Do not reuse `OPENAI_API_KEY` as Noema's gateway token. - -## How a host calls it - -The public HTTP surface is documented in [`openapi.json`](./openapi.json) -([OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.1.html)) and -[API 명세](./docs/api-spec.md). Every JSON response is -`{ ok: true, data, trace_id }` or -`{ ok: false, error_code, message, details, trace_id }`. +After deployment, configure `NOEMA_EXCHANGE_URL` in the trusted central workflow or consuming deployment to an `https://` URL targeting the Worker `/exchange` endpoint. Plain HTTP is permitted only for loopback testing. -| Method | Path | Purpose | -| --- | --- | --- | -| `GET` | `/health` | Liveness only. Does not prove credential-exchange readiness. | -| `GET` / `HEAD` | `/ready` | Unauthenticated runtime readiness. Incomplete config returns `503 ERR_SERVICE_NOT_READY` without reflecting secrets. | -| `POST` | `/exchange` | Exchange a GitHub Actions OIDC bearer for a short-lived installation token. | +Do not reuse an upstream provider credential such as `OPENAI_API_KEY` as Noema's gateway token. -Example liveness check: +### Smoke-check a deployment ```bash -curl -sS "$NOEMA_BASE_URL/health" +NOEMA_EXCHANGE_URL=https://example.workers.dev/exchange npm run smoke:check ``` -Example exchange (OIDC bearer from GitHub Actions; JSON body optional, max -8,192 UTF-8 bytes): +The smoke check exercises liveness, readiness, exchange framing, unauthenticated challenge behavior, and bounded response/security headers. It does not manufacture GitHub App, deployment, or production-readiness evidence that is absent from the environment. + +## Example exchange + +A trusted GitHub Actions job supplies its OIDC bearer and the target repository: ```bash curl -sS -X POST "$NOEMA_EXCHANGE_URL" \ @@ -122,82 +121,122 @@ curl -sS -X POST "$NOEMA_EXCHANGE_URL" \ -d '{"target_repository":"ContextualWisdomLab/example"}' ``` -`target_repository` must be a string `owner/name` in the allowed organization. -`/exchange` accepts only `POST` (`Allow: POST` on 405), returns Bearer -challenges on 401 (`invalid_request` vs `invalid_token`), and includes -`Cache-Control: no-store`, `Pragma: no-cache`, `X-Content-Type-Options: nosniff`, -`X-Trace-Id`, and `X-Latency-Ms`. Issued and inbound tokens must not appear in -logs. +`target_repository` must be an allowed `owner/name` repository. Noema validates the configured issuer, audience, organization, exact trusted workflow identity, immutable workflow-source identity, token time semantics, and replay boundary before credential exchange. + +Issued and inbound credentials must not appear in logs or retained model context. + +## Architecture at a glance + +```text +GitHub Actions + │ + │ OIDC identity + ▼ +┌───────────────────────────────┐ +│ Noema │ +│ credential + maintenance │ +│ control plane │ +├───────────────────────────────┤ +│ OIDC trust verification │ +│ replay / rate-limit boundary │ +│ GitHub App token exchange │ +│ exact-revision evidence │ +│ review / maintenance controls │ +└───────────────┬───────────────┘ + │ + short-lived scoped capability + │ + ▼ + GitHub repository + +Model-backed judgement ──► contextual-orchestrator + (provider/routing owner) +``` -A host that only needs the LLM gateway copies -`contracts/orchestrator-gateway.json` (or calls -`node scripts/verify-orchestrator-gateway.mjs --print-contract`) and uses the -same `NOEMA_LLM_*` settings. Do not copy Noema's OIDC broker, GitHub App -identities, or sandbox/runner isolation into the host. +The Worker, GitHub App capability boundary, maintenance controls, and review evidence are Noema-owned responsibilities. Adjacent products remain independently deployable and integrate through published contracts rather than shared application tables or ambient credentials. -The Python reviewer package (`reviewer/`) is the judgement plane. It consumes a -bounded PR manifest and can publish a `ReviewVerdict`. See -[`reviewer/README.md`](./reviewer/README.md). +## GitHub App permissions -After deploy, confirm the HTTP contract: +The deployed GitHub App requires the narrow repository permissions used by the exchange/review boundary: -```bash -NOEMA_EXCHANGE_URL=https://.../exchange npm run smoke:check -``` +- Pull requests: read and write +- Checks: read-only +- Contents: read-only -`npm run smoke:check` checks `/health`, `/ready`, and `/exchange` schema, -trace/latency headers, runtime readiness, the unauthenticated 401 Bearer -challenge, and no-store/nosniff headers. +Install the App only on repositories that need the trusted workflow path. -## Required GitHub App permissions +## Security model -Repository permissions: +Noema's default posture is conservative: -- Pull requests: Read and write -- Checks: Read-only -- Contents: Read-only +- short-lived capability instead of broad long-lived repository credentials; +- exact workflow and repository identity before token minting; +- replay protection and bounded pre-auth rate limiting; +- no model/provider credential ownership in this repository; +- no credential-bearing execution of untrusted PR/model output; +- no transfer of predecessor-head checks or reviews after a source change; +- missing, malformed, stale, partial, pending, or ambiguous evidence is non-passing; +- formal review, merge, release, deployment, and commercial/legal authority remain separate. -Install the app on `ContextualWisdomLab/.github` and on target repositories -that use the central required workflow. +See [`docs/threat-model.md`](./docs/threat-model.md), [`docs/doctoring/architecture-trust-boundaries.md`](./docs/doctoring/architecture-trust-boundaries.md), and the [`docs/adr/`](./docs/adr/) decision records for the deeper trust model. -## Operator configuration +## Verify the repository -Public Worker vars (defaults in `wrangler.toml`): +The ordinary contributor checks are explicit package scripts: -| Variable | Default | Role | -| --- | --- | --- | -| `ALLOWED_ISSUER` | `https://token.actions.githubusercontent.com` | GitHub Actions OIDC issuer | -| `ALLOWED_AUDIENCE` | `cwl-noema-review` | OIDC audience | -| `ALLOWED_REPOSITORY_OWNER` | `ContextualWisdomLab` | Allowed org | -| `ALLOWED_WORKFLOW_REPOSITORY` | `ContextualWisdomLab/.github` | Trusted workflow repo | -| `ALLOWED_WORKFLOW_REF_PREFIX` | `ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main` | Exact trusted workflow ref (name kept; matching is exact, not prefix) | -| `GITHUB_API_BASE` | `https://api.github.com` | GitHub Cloud API origin | -| `NOEMA_RATE_LIMIT_PER_MINUTE` | `60` | `/exchange` fixed-window budget | -| `NOEMA_OIDC_JWKS_CACHE_TTL_SECONDS` | `300` | OIDC JWKS cache | -| `NOEMA_INSTALLATION_CACHE_TTL_SECONDS` | `600` | Installation-id cache | - -`/exchange` first applies a SQLite-backed Durable Object fixed-window limit -across Worker isolates, then keeps the isolate-local limiter as defense in -depth. Missing or malformed distributed decisions fail closed; see -[Distributed rate limiting](./docs/distributed-rate-limiting.md). -OIDC `jti` values are consumed once by a Durable Object replay guard; see -[OIDC replay protection](./docs/oidc-replay-protection.md). - -## Operator documentation - -- [온보딩 가이드](./docs/onboarding.md) -- [운영 Runbook](./docs/runbook.md) -- [API 명세](./docs/api-spec.md) -- [안정성 계약](./docs/api-stability-contract.md) -- [OpenAPI](./openapi.json) -- [보안/위협 모델](./docs/threat-model.md) -- [배포 가이드](./docs/deployment-guide.md) -- [SLA/지원 정책](./docs/sla-and-support.md) -- [Runtime readiness](./docs/runtime-readiness.md) -- [Distributed Rate Limiting](./docs/distributed-rate-limiting.md) -- [Orchestrator gateway consumer contract](./docs/orchestrator-gateway-consumer-contract.md) - -Maintainers and coding agents: start at -[`docs/internal/README.md`](./docs/internal/README.md) -(contributor and agent procedure: -[`docs/development/contributor-and-agent-procedure.md`](./docs/development/contributor-and-agent-procedure.md)). +```bash +npm run typecheck +npm test +npm run security:scan +``` + +For the broader evidence-bearing release verification path: + +```bash +npm run release:verify +``` + +`release:verify` combines type checking, tests, security scanning, KPI evidence, dependency-license inventory, and acquisition-manifest/integrity checks. Passing repository checks are technical evidence; they are not by themselves a deployment, certification, approval, sale, or legal claim. + +## Documentation map + +Start with the document that matches the job at hand: + +| Goal | Document | +| --- | --- | +| Product requirements and non-goals | [`docs/PRD.md`](./docs/PRD.md) | +| Technical requirements | [`docs/TRD.md`](./docs/TRD.md) | +| Architecture and trust boundaries | [`ARCHITECTURE.md`](./ARCHITECTURE.md) | +| Architecture decisions | [`docs/adr/README.md`](./docs/adr/README.md) | +| API contract | [`docs/api-spec.md`](./docs/api-spec.md) / [`openapi.json`](./openapi.json) | +| Deployment | [`docs/deployment-guide.md`](./docs/deployment-guide.md) | +| Operations | [`docs/runbook.md`](./docs/runbook.md) | +| Scheduled product maintenance | [`.github/workflows/hourly-product-development.yml`](./.github/workflows/hourly-product-development.yml) — implementation entry point; operating procedure stays in the contributor/internal docs | +| Security / threat model | [`docs/threat-model.md`](./docs/threat-model.md) | +| Runtime readiness | [`docs/runtime-readiness.md`](./docs/runtime-readiness.md) | +| Licensing and IP transfer | [`docs/LICENSING_AND_IP_TRANSFER.md`](./docs/LICENSING_AND_IP_TRANSFER.md) | +| Requirement → evidence traceability | [`docs/TRACEABILITY.md`](./docs/TRACEABILITY.md) | +| Current product/technical gaps | [`docs/product-technical-gap-baseline.md`](./docs/product-technical-gap-baseline.md) | +| Full documentation index | [`docs/README.md`](./docs/README.md) | + +Maintainers and coding agents should also read [`docs/internal/README.md`](./docs/internal/README.md) and [`docs/development/contributor-and-agent-procedure.md`](./docs/development/contributor-and-agent-procedure.md) before changing repository behavior. + +## Product principles + +1. **Least privilege.** Capability is bounded by purpose, repository, role, operation, and lifetime. +2. **Exact revision before authority.** Mutable identities are re-read before privileged decisions and writes. +3. **Evidence is not authority.** Green-looking signals remain distinct until the correct authority interprets them. +4. **Fail closed.** Missing or ambiguous evidence is never upgraded into success. +5. **Standalone first, composable second.** Noema remains independently deployable and integrates through versioned contracts. +6. **Work conserving.** A waiting lane blocks only that lane; unrelated safe work continues. +7. **Claims stay evidence-bound.** Documentation never substitutes for real deployment, customer, revenue, legal, ownership, or transfer evidence. + +## Contributing + +Before changing behavior, read [`AGENTS.md`](./AGENTS.md), the canonical PRD/TRD, architecture decisions, and the current product-gap evidence. Keep runtime/security changes test-first, preserve exact-head evidence boundaries, and update the public contract and operator documentation whenever externally visible behavior changes. + +## License + +Noema source is licensed under the [Apache License 2.0](LICENSE). `package.json` is marked `private` because the npm package is not the product distribution channel; `private` does not narrow the Apache-2.0 source grant. + +Third-party development and build tooling retains its own license terms and is not relicensed by this repository. Release and transfer evidence must continue to inventory those obligations separately rather than treating the Noema source license as a license for dependencies or bundled artifacts. diff --git a/docs/LICENSING_AND_IP_TRANSFER.md b/docs/LICENSING_AND_IP_TRANSFER.md index 94f52e3a9..34e310d2a 100644 --- a/docs/LICENSING_AND_IP_TRANSFER.md +++ b/docs/LICENSING_AND_IP_TRANSFER.md @@ -1,55 +1,61 @@ # Noema Licensing and IP Transfer -- **Status:** Protected policy/evidence baseline; not legal clearance. Active PR #495 adds the npm dependency-license inventory generator described in section 4. +- **Status:** Repository rights policy/evidence baseline; source-license decision is Apache-2.0 on PR #530 until protected integration. This is not acquisition or transfer legal clearance. - **Scope:** Noema source rights, package/container metadata, third-party obligations, contributor/IP provenance, release distribution, and acquisition transfer evidence. -- **Decision authority:** Repository automation may detect, authenticate, inventory, and compare evidence. The outbound-license and transfer-rights decision belongs to the authorized **owner/legal** function. +- **Decision authority:** Repository automation may detect, authenticate, inventory, and compare evidence. The repository owner has explicitly selected Apache License 2.0 for Noema source; future outbound-license changes and transfer-rights decisions remain owner/legal governance actions. ## 1. Core invariant -**Public source availability is not a grant of rights.** Repository visibility, cloneability, package installation, an SBOM, scanner output, or successful CI does not establish permission to use, modify, redistribute, sublicense, or transfer Noema. +**Public source availability is not a grant of rights by itself.** The grant comes from the controlling repository rights file. On PR #530, root `LICENSE` declares Apache-2.0 for Noema source. Until that exact head integrates, protected `main` remains the currently shipped source-rights authority. -Noema therefore keeps legal authority separate from technical evidence: +Noema keeps source licensing, package publication, third-party obligations, and transfer authority separate: - automation may inventory, hash, compare, and **fail closed** on missing, ambiguous, or contradictory rights evidence; -- automation must never infer or silently choose an outbound license; -- an explicit owner/legal decision is required before a license posture is represented as approved; -- missing, unknown, incompatible, or contradictory rights evidence blocks distribution/acquisition claims rather than becoming a technical PASS. +- automation must never infer or silently change an outbound license; +- the explicit owner decision for Noema source is Apache-2.0 and is represented by the root `LICENSE` plus customer-facing README statement; +- `package.json` intentionally remains `private` and is not the product distribution channel, so the source grant does not depend on publishing npm license metadata; +- dependency/tool licenses are not relicensed by the Noema source grant; +- missing, unknown, incompatible, or contradictory third-party or transfer evidence blocks distribution/acquisition claims rather than becoming a technical PASS. This document is not legal advice. It specifies the evidence and authority boundary enforced by Noema technical controls. ## 2. Repository rights declaration -An approved outbound posture must be discoverable in repository source and agree across release surfaces. +An approved outbound posture must be discoverable in repository source and remain unambiguous across any surface that actually distributes the work. -At minimum: +For the current owner decision: -1. a root `LICENSE` file or explicitly approved custom-rights file contains controlling source terms when repository text grants rights; -2. `package.json` expresses the same declared posture without inventing broader rights; -3. release/container metadata that declares rights expresses the same approved posture for that exact artifact/revision; -4. decision owner, approval record, effective date, and scope are retained as acquisition evidence instead of inferred from a filename; -5. licensing changes are governance changes requiring reviewed source mutation, release-impact analysis, and evidence regeneration. +1. root `LICENSE` contains Apache License 2.0 for Noema source; +2. root `README.md` identifies Apache-2.0 as the source license; +3. `package.json` remains `"private": true` and intentionally omits a package-publication license field because the npm package is not the distribution channel; this keeps `package.json` and `package-lock.json` lock metadata stable rather than implying a package release that does not exist; +4. if npm package publication is introduced later, package metadata and lockfile root metadata must be regenerated together and must agree with the approved rights posture before publication; +5. release/container metadata that declares rights must express the same source posture for the exact artifact/revision only when that artifact is actually covered by the same grant; +6. third-party licenses and notices remain separate evidence and are never absorbed into Apache-2.0 by metadata; +7. licensing changes are governance changes requiring reviewed source mutation, release-impact analysis, and evidence regeneration. ### 2.1 `package.json` alignment +For a package that is actually distributed through npm: + - use a valid **SPDX** expression when approved terms have one; - use `SEE LICENSE IN ` for approved custom terms stored in a bounded repository file; -- use `UNLICENSED` when package metadata intentionally grants no use rights; -- `"private": true` is a publication safeguard, not an outbound-rights decision. +- use `UNLICENSED` only when package metadata intentionally grants no use rights; +- regenerate `package-lock.json` whenever root package metadata changes so tracked lock metadata stays exact. -Automation may verify syntax, paths, hashes, and declared relationships. It must not infer legal equivalence from filenames or metadata labels alone. +For current Noema, `"private": true` plus absence of an npm distribution channel means root `LICENSE` is the controlling source grant. `private` itself is still only a publication safeguard; it neither grants nor narrows Apache-2.0 source rights. ### 2.2 OCI and release metadata **OCI image license metadata** and equivalent archive/package/registry fields are artifact claims, not independent legal authority. -- `org.opencontainers.image.licenses` or an equivalent field must agree with the owner/legal decision and repository/package declaration for that exact release scope; -- while the outbound-rights decision is unresolved, artifact license metadata **must remain absent** unless an authorized decision explicitly requires a truthful bounded declaration; +- `org.opencontainers.image.licenses` or an equivalent field must agree with the source-rights decision only when the exact artifact is covered by that source grant and its bundled third-party obligations have been accounted for; +- a source-level Apache-2.0 declaration does not license third-party binaries, dependencies, base images, fonts, models, datasets, or assets; - invented `LicenseRef-*`, repository visibility, `private: true`, SBOM guesses, or scanner classifications cannot create rights; - source/revision/provenance labels that do not claim licensing authority may remain when truthful and exact-revision bound. ## 3. Protected exact-release `artifact_rights_metadata` contract -Protected source implements an exact-release rights receipt named `artifact_rights_metadata`. The acquisition-integrity work that introduced this boundary is already integrated on protected main. The receipt is technical evidence, never legal authority. +Protected source implements an exact-release rights receipt named `artifact_rights_metadata`. The acquisition-integrity work that introduced this boundary is already integrated on protected main. The receipt is technical evidence, never independent legal authority. The authenticated receipt binds at least: @@ -67,8 +73,8 @@ The parser/evidence boundary is fail closed: - reject malformed JSON; - reject **duplicate** decoded JSON keys before ordinary object parsing can select a last value; - reject symlink/path/descriptor substitution and digest mismatch; -- reject an OCI license annotation under unresolved/custom/no-rights authority when the approved contract requires no annotation; -- when an approved SPDX expression exists, any artifact license annotation must match that expression exactly. +- reject artifact annotations that contradict the approved repository/artifact rights decision; +- when an approved SPDX expression applies to an exact artifact, any artifact license annotation must match that expression exactly. A receipt, scanner, SBOM, or annotation never creates owner/legal authority. It proves only identity and consistency with an already approved decision. @@ -79,13 +85,19 @@ Every distributable or transferable exact release must bind third-party rights e Required evidence includes: - exact-release **SBOM** and dependency graph; -- dependency-license inventory for direct, transitive, bundled, static, and runtime assets where applicable; +- dependency-license inventory for direct, transitive, bundled, static, runtime, development, and build assets where policy requires it; - required attribution and **NOTICE** material, preserving upstream notices when terms require them; - explicit disposition for unknown, custom, copyleft, source-available, dual-licensed, or otherwise policy-sensitive terms; - classifier/scanner tool identity and evidence source; - hashes/immutable identities tying license and NOTICE artifacts to the release/SBOM. -PR #495 adds `npm run release:dependency-license-inventory` for the npm lockfile slice. The generated `artifacts/release/dependency-licenses.json` is deterministic and bound to the SHA-256 of the exact `package-lock.json`. It records each non-root locked package path, package name, version, declared license, resolved artifact, integrity value, npm `dev`/`optional`/`devOptional`/`inBundle` classification authority, install-script authority (`hasInstallScript`), and present npm platform constraints (`cpu`/`os`). Present boolean authority must remain boolean; `devOptional`, `inBundle`, and `hasInstallScript` are preserved as `dev_optional`, `in_bundle`, and `has_install_script`. Present `cpu`/`os` constraints must be non-empty arrays of canonical non-empty strings, so acquisition evidence cannot silently discard or normalize platform applicability. Resolved artifact authority must be a canonical inspectable URI and must not embed URL passwords, non-conventional URL usernames, authentication/token/secret/key/signature/credential parameters in either query or fragment, or compact signed-URL `sig` parameters; the conventional `git` username is retained only for SSH-like Git URLs. Otherwise buyer/release evidence fails closed rather than retaining credential-bearing source metadata. Duplicate-key or malformed lockfiles and missing package identity/license fields fail closed. This inventory records package metadata; it does **not** establish compatibility, satisfy upstream NOTICE obligations, or create owner/legal permission. +`npm run release:dependency-license-inventory` produces deterministic lockfile-bound inventory evidence. The inventory records package metadata; it does **not** establish compatibility, satisfy upstream NOTICE obligations, or create owner/legal permission. + +### 4.1 Current GPL-family tooling finding + +The current `package-lock.json` contains optional development/build packages on the `wrangler → miniflare → sharp → @img/sharp-libvips-*` path whose declared license is `LGPL-3.0-or-later`; `@img/sharp-wasm32` declares `Apache-2.0 AND LGPL-3.0-or-later AND MIT`. These packages are not relicensed by Noema's Apache-2.0 source license. + +Repository evidence also shows that the patch-validator runtime-image boundary explicitly excludes `wrangler`, `workerd`, and `miniflare`; therefore this finding must not be overstated as proof that LGPL code is bundled into that runtime image. It is nevertheless an inbound development/build-tooling policy gap because ContextualWisdomLab does not accept GPL-family software as the normal dependency baseline. Distribution/acquisition readiness must remain fail closed until issue #531 removes/replaces this dependency path or an explicit repository-level exception is approved for the exact use and distribution model. Unknown or unresolved obligations fail closed for distribution/acquisition readiness. Vulnerability or provenance success does not prove license compatibility. @@ -101,7 +113,7 @@ Acquisition readiness requires evidence that the seller has authority to transfe - vendored/copied third-party code outside the dependency graph; - trademark, domain, signing key, GitHub App, cloud account, and other operational ownership needed to transfer the running product. -A Git commit proves repository history, not legal ownership. Missing provenance remains external evidence and must not be synthesized. +A Git commit proves repository history, not legal ownership. Missing transfer provenance remains external evidence and must not be synthesized. The Apache-2.0 source grant does not by itself prove acquisition-transfer ownership. ## 6. Acquisition transfer evidence @@ -112,7 +124,7 @@ The machine-checkable transfer contract binds, at minimum: - repository identity and exact source/release revision; - approved owner/legal decision identifier; - controlling `LICENSE`/custom-rights file path and SHA-256 when applicable; -- `package.json` rights declaration plus package-metadata hash; +- package-publication rights declaration plus metadata hash when a package is actually distributed; - exact-release `artifact_rights_metadata` path and SHA-256 when an artifact exposes rights metadata; - exact-release SBOM identity; - dependency-license and NOTICE/attribution artifact identities; @@ -120,20 +132,20 @@ The machine-checkable transfer contract binds, at minimum: - outstanding exceptions/legal holds/unresolved third-party terms; - evidence owner, review timestamp, and retention/rotation policy. -Protected acquisition-integrity code authenticates this consistency boundary and rejects parser ambiguity, but it does not choose the legal posture. +Protected acquisition-integrity code authenticates this consistency boundary and rejects parser ambiguity, but it does not manufacture transfer authority. ## 7. Release and acquisition gates ### Release-distribution gate -Before publishing an artifact, the exact integrated protected source must have a reviewed distribution posture and applicable third-party obligations for that artifact. Internal test builds do not create distribution rights. If a package/image/archive carries a rights field, it must agree with approved source/package posture and exact-release `artifact_rights_metadata` before publication. +Before publishing an artifact, the exact integrated protected source must have a reviewed distribution posture and applicable third-party obligations for that artifact. Internal test builds do not create distribution rights. If a package/image/archive carries a rights field, it must agree with approved source posture and exact-release `artifact_rights_metadata` before publication. ### Acquisition final gate ```text -owner/legal decision +owner source-license decision → repository rights file -→ package.json rights metadata +→ package-publication metadata when applicable → exact-release artifact_rights_metadata when applicable → release/container rights metadata when present → exact-release SBOM @@ -145,29 +157,35 @@ owner/legal decision Each arrow requires independent identity/consistency evidence. A mismatch, missing required record, malformed/ambiguous JSON, or unresolved right is a fail-closed condition. -## 8. Current evidence and residual gap — 2026-08-23 +## 8. Current evidence and residual gap — 2026-09-01 -Protected `main` at `e8a816e9f33ba1905b9fcc258798e07a3cd2657f` has no root `LICENSE` file. Protected `package.json` is `"private": true` and has no `license` field. That is evidence of an unresolved licensing/IP-transfer decision, not evidence for MIT, Apache-2.0, proprietary, source-available, or another posture. +Protected `main@03ef2301bad020b9ab4dfde2ec3c4e7f460024ca` still has no root `LICENSE`. PR #530 now carries the explicit owner-selected Apache-2.0 source posture: -Current technical state is intentionally split: +- root `LICENSE`: Apache License 2.0; +- root `README.md`: customer-facing Apache-2.0 source-license statement and separate third-party obligation boundary; +- `package.json`: remains private and lock-stable; no npm package distribution claim is introduced. -- protected acquisition-integrity code binds `artifact_rights_metadata` to repository/release/artifact identity and rejects duplicate decoded keys, malformed UTF-8/JSON, and inconsistent rights metadata before it can become acquisition evidence; -- protected #407 integration supplies the patch-validator image/runtime/supply-chain implementation and its local SBOM/provenance verification boundary; -- protected #493 integration rejects placeholder, ambiguous, loopback and local-only pilot/commercial authority from saleable-readiness evidence; -- protected #494 integration binds Noema's immutable OIDC workflow trust anchor to the current audited central workflow source identity; -- active PR #495 adds reproducible npm lockfile license inventory generation, exact coverage ownership, canonical package-path and resolved-artifact authority, credential-free artifact metadata, npm dependency/distribution classification, install-script authority, and `cpu`/`os` platform applicability. Until it integrates, that generator remains active-PR truth, not protected-main truth; -- no technical artifact resolves compatibility, upstream NOTICE retention, contributor ownership/assignment, or the outbound-rights decision. +Those declarations are candidate truth until #530 integrates; they are not predecessor evidence for protected main. -Issue #5 carries the acquisition owner/legal and ownership/assignment evidence gap. Issue #66 carries remaining release/publication, NOTICE and provenance/activation boundaries. Neither documentation nor technical enforcement makes legal clearance pass. +Current residual gaps remain deliberately separate: + +- the lockfile contains the GPL-family development/build tooling path described in §4.1 and therefore does not yet satisfy the organization default inbound-license policy; +- exact-release dependency/NOTICE evidence must still prove the actual distributed artifact contents; +- contributor ownership/assignment and acquisition-transfer evidence remain separate from source licensing; +- release/publication/deployment evidence remains separate from repository-source rights; +- no source file, README sentence, scanner result, or successful CI run may upgrade those missing evidence classes into a commercial or legal PASS. + +Issue #5 carries acquisition owner/legal and ownership/assignment evidence. Issue #66 carries remaining release/publication, NOTICE and provenance/activation boundaries. Issue #531 owns the GPL-family development/build-tool replacement. The source-license decision narrows the gap but does not close those issues. ## 9. Non-goals Noema automation must not: -- pick an outbound license because it appears commercially convenient; +- silently choose or change an outbound license without an owner-authorized source change; - emit package/container/release license fields merely to make metadata look complete; - treat `UNLICENSED`, `private`, repository visibility, `LicenseRef-*`, or a copyright notice as interchangeable; -- infer license compatibility from filenames or scanner guesses alone; +- infer dependency-license compatibility from filenames or scanner guesses alone; +- treat the repository's Apache-2.0 source license as a license for third-party packages or bundled artifacts; - fabricate contributor consent, employment ownership, contractor assignment, or third-party permission; - remove NOTICE/attribution obligations to make an audit pass; - accept duplicate-key or malformed evidence because one parser selects a convenient last value; @@ -177,6 +195,8 @@ Noema automation must not: GitHub. (2026). *Licensing a repository*. GitHub Docs. https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository +The Apache Software Foundation. (2004). *Apache License, Version 2.0*. https://www.apache.org/licenses/LICENSE-2.0 + npm, Inc. (2026). *package-lock.json*. npm Docs. https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json npm, Inc. (2026). *package.json — license*. npm Docs. https://docs.npmjs.com/files/package.json/ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..d0f0d5074 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,37 @@ +--- +title: Noema +--- + +# Noema + +Noema is an evidence-producing credential and maintenance control plane for governed GitHub automation. It gives repository automation short-lived, repository-scoped capability without turning model output, CI status, or long-lived secrets into authority. + +## Start here + +For local development and verification, follow the [README](https://github.com/ContextualWisdomLab/noema#readme). The deployed product is the Cloudflare Worker and its HTTP/evidence contracts; the repository package itself is private. + +## Product responsibility + +Noema owns GitHub Actions OIDC trust verification, GitHub App capability exchange, exact-revision maintenance controls, bounded review evidence, and the operational evidence needed to keep automation fail-closed and least-privilege. + +Model discovery and provider routing belong to [contextual-orchestrator](https://github.com/ContextualWisdomLab/contextual-orchestrator). Noema keeps checks, reviews, merge authority, releases, deployments, and commercial/legal evidence as separate trust domains rather than promoting one signal into another. + +## Documentation and operations + +- [README](https://github.com/ContextualWisdomLab/noema#readme) — product value, quick start, architecture, and security posture. +- [Product requirements](https://github.com/ContextualWisdomLab/noema/blob/main/docs/PRD.md) — product responsibility, users, jobs, and non-goals. +- [Architecture](https://github.com/ContextualWisdomLab/noema/blob/main/ARCHITECTURE.md) — canonical architecture and trust boundaries. +- [API specification](https://github.com/ContextualWisdomLab/noema/blob/main/docs/api-spec.md) and [OpenAPI](https://github.com/ContextualWisdomLab/noema/blob/main/openapi.json) — public HTTP contract. +- [Deployment guide](https://github.com/ContextualWisdomLab/noema/blob/main/docs/deployment-guide.md) — deployment procedure. +- [Operations runbook](https://github.com/ContextualWisdomLab/noema/blob/main/docs/runbook.md) — operating and recovery guidance. +- [Threat model](https://github.com/ContextualWisdomLab/noema/blob/main/docs/threat-model.md) — security boundaries and failure modes. +- [Traceability](https://github.com/ContextualWisdomLab/noema/blob/main/docs/TRACEABILITY.md) — requirement-to-evidence mapping. +- [Current product/technical gaps](https://github.com/ContextualWisdomLab/noema/blob/main/docs/product-technical-gap-baseline.md) — evidence-backed remaining work. +- [Releases](https://github.com/ContextualWisdomLab/noema/releases) — published release history when available. +- [Ask DeepWiki](https://deepwiki.com/ContextualWisdomLab/noema) — repository-grounded questions and code navigation. + +## Evidence boundary + +Passing repository checks is technical evidence, not a deployment, certification, approval, sale, or legal claim. A capability is repository-facing only after the relevant protected-branch, deployment, and live-state evidence exists. + +This file is a public documentation landing source. GitHub Pages publication is a separate repository-facing state and must be verified live before it is claimed available. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 79c3189a0..b748d67da 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -4,15 +4,16 @@ 이 문서는 제품 요구, 구현, 검증, 운영 증거 사이의 현재 차이를 한곳에서 추적한다. 저장소 파일과 테스트는 revision-local 또는 protected-source 구현만 증명한다. PR 상태는 exact head와 live base에서, 운영·배포·고객·매출·법적 증거는 해당 외부 권한에서 각각 다시 확인해야 한다. 문서나 성공 boolean만으로 이후 단계의 증거를 만들지 않는다. -이 baseline의 source snapshot은 protected `main` `8ae8f5eee4e913a8ee106c3e2e550b0c9316c0e4`이며, issues #3, #5, #27, #29, #66, #227은 2026-09-01 KST에 GitHub에서 모두 `OPEN`으로 다시 확인했다. 상태가 바뀌면 live GitHub를 우선하며 이 표를 갱신한다. +이 baseline의 protected-source snapshot은 `main@5aad3e410703faaf52882e2f33fadd25d217bcdd`이며, README/license candidate truth는 PR #530 exact head에만 적용한다. issues #3, #5, #27, #29, #66, #227, #531의 live 상태를 GitHub 권위로 다시 읽어야 하며, protected/main·PR·외부 증거를 서로 대체하지 않는다. ## Live external observation — 2026-09-01 KST | Authority | Observation | Consequence | | --- | --- | --- | -| Pull requests | #510, #512, #513, #521 are open; #510/#512/#521의 exact-head Application·reviewer-ci·Security Scan은 terminal-success이고 image gate는 `in_progress`, #513의 current exact-head gates는 `queued` | zero-PR hourly activation canary와 merge completion은 아직 입증되지 않았다 | -| Hourly product development | scheduled run `33408669511` stopped at the zero-open-PR gate; gateway, OpenCode, package, and publication stages were skipped | retired direct-provider/NVIDIA run을 현재 상태로 사용하지 않으며, zero-PR 이후 `contextual-orchestrator` canary가 필요하다 | -| Release/publication | GitHub release와 protected-main patch-validator workflow-dispatch receipt가 없다 | immutable publication, signing, deployment, KPI, acquisition evidence는 계속 미완료다 | +| README/license lane | PR #530 is open and carries the product-first README plus Apache-2.0 root source grant; every push invalidates predecessor-head checks | protected main remains unlicensed until the unchanged exact head integrates | +| npm package boundary | `package.json` remains `private` and the npm package is not a product distribution channel; no package-publication license field is introduced | root `LICENSE` controls source rights without forcing unrelated lockfile metadata churn | +| Dependency licensing | `package-lock.json` contains `LGPL-3.0-or-later` optional dev/build packages on `wrangler → miniflare → sharp → @img/sharp-libvips-*`; issue #531 owns removal/replacement | source Apache-2.0 does not make the current toolchain compliant with the organization no-GPL-family default | +| Release/publication | immutable release/deployment/customer/revenue/transfer evidence remains a separate authority class | source licensing cannot be promoted into acquisition readiness | ## Current baseline @@ -22,6 +23,8 @@ | Reviewer and maintenance control plane | 독립 App identity, bounded manifest, deterministic fail-closed gates | `reviewer/noema_reviewer/`, maintainer/reviewer workflows, capability-file ingress | reviewer tests, workflow contract tests, current-head review artifacts | Maintainer/Reviewer App 설치·권한·key custody·rotation 및 publication identity | Source contract implemented; external activation evidence is open | | Hourly product-development loop | `contextual-orchestrator` inference와 별도 Maintainer App publication identity를 사용하는 work-conserving loop | `.github/workflows/hourly-product-development.yml`, orchestrator gateway contract, publication/readiness validators | workflow shape, gateway preflight, lease, publication prerequisite and stale-head refusal tests | zero-PR scheduled proposal publication과 rollback/recovery exercise | Implemented source; production activation incomplete | | Patch-validator supply chain | exact source/image/receipt binding과 fail-closed vulnerability policy | `Dockerfile.patch-validator`, image workflow, validator/SBOM/receipt modules | build, runtime, smoke, SBOM, vulnerability and receipt tests | protected-main operational receipt와 registry publication/signing/attestation | Implemented source; operational/publication evidence incomplete | +| Source licensing | Noema-owned source uses one explicit commercial-friendly outbound grant; package publication and dependencies retain independent terms | PR #530 `LICENSE`, root `README.md`, `docs/LICENSING_AND_IP_TRANSFER.md`; private `package.json` remains non-distribution metadata | exact-head repository/doc/test consistency | protected integration plus third-party/tooling policy resolution | Apache-2.0 candidate truth on #530; not yet protected truth | +| Third-party/tooling licensing | GPL-family packages are not accepted as the normal inbound dependency baseline | current lockfile + dependency-license inventory + issue #531 | exact lockfile scan/inventory must become free of GPL/LGPL/AGPL toolchain entries | commercially compatible Wrangler/Miniflare/build-tool replacement or exact approved exception | Open compliance gap; source license does not resolve it | | Release and deployment | source → package/SBOM/provenance → immutable publication → deployment/rollback | release, publication, deployment and readiness scripts | exact-source/reproducibility/receipt/rollback contract tests | immutable release, protected deployment, recovery and production smoke evidence | Incomplete; repository evidence cannot establish deployment | | KPI, customer and acquisition | authentic evidence must retain source, time and buyer/legal authority | KPI, acquisition manifest/integrity/readiness and license validators | bounded input, provenance, ordering, integrity and fail-closed tests | authentic 30-day production KPI, customer/revenue and transfer evidence | Incomplete; no commercial-readiness claim | @@ -29,16 +32,18 @@ | Priority | Gap | Buyer/operator impact | Current owner | Authoritative completion evidence | Next executable action | | --- | --- | --- | --- | --- | --- | +| P0 | GPL-family development/build dependency path | 조직의 상업용 inbound 정책과 현재 npm toolchain이 충돌한다 | issue #531 | exact-head `package-lock.json`과 dependency inventory에서 GPL/LGPL/AGPL 경로가 사라지고 Worker dev/deploy·typecheck·tests·security가 그대로 통과 | Wrangler/Miniflare/Sharp 경로를 상업적으로 호환되는 도구 경계로 교체하고 lockfile을 재검증한다 | | P0 | Maintainer/Reviewer App 및 hourly publication identity 활성화 | 자동 유지보수와 독립 리뷰가 production capability로 동작한다는 증거가 없다 | issues #29 / #227 | 현재 App 설치·권한·key custody/rotation, 성공한 scheduled publication artifact와 rollback 결과 | 외부 App 구성을 완료한 뒤 readiness와 scheduled run을 실행하고 artifact를 보존한다 | | P0 | protected `main` governance 목표와 live policy 정합성 | source 검증만으로 실제 merge/release 통제를 보장할 수 없다 | issue #27 | live ruleset/branch-protection API와 관찰된 required workflow/status 결과 | governance audit을 live policy에 실행하고 차이를 owning control에서 수정한다 | +| P1 | Apache-2.0 source grant integration | 공개 저장소가 protected main에서는 아직 명시적 사용권을 제공하지 않는다 | PR #530 | unchanged exact-head README/LICENSE + applicable reviews/checks + protected merge | #530 exact head를 정상 protected path로 통합한다 | | P1 | patch-validator 운영·배포 증거 | 검증된 source image가 실제 배포·서명·활성화됐는지 구매자가 확인할 수 없다 | issue #66 | protected-main operational receipt, registry digest, signature/attestation과 activation proof | exact protected source에서 publication pipeline을 실행한다 | | P1 | authentic 30-day KPI | 신뢰성·성능·운영가치를 fixture가 아닌 실운영 자료로 입증하지 못한다 | issue #3 | production-origin, time-bound, integrity-checked 30-day KPI evidence | 승인된 production source에서 collector와 verifier를 실행한다 | | P1 | release/deployment/acquisition evidence | buyer/legal/commercial 권한이 없어 매각 readiness를 선언할 수 없다 | issue #5 | immutable release/deployment/customer/revenue/legal transfer evidence | 앞선 evidence family를 순서대로 충족하고 acquisition audit을 재실행한다 | ## Documentation contradictions -과거 PR 번호와 당시 상태는 historical provenance일 뿐 현재 owner나 구현 상태가 아니다. Canonical TRD와 ADR은 protected implementation surface와 durable live issue owner를 사용하며, historical PR을 current owner로 사용하지 않는다. +과거 PR 번호와 당시 상태는 historical provenance일 뿐 현재 owner나 구현 상태가 아니다. Canonical TRD와 ADR은 protected implementation surface와 durable live issue owner를 사용하며, historical PR을 current owner로 사용하지 않는다. PR #530의 Apache-2.0 grant도 merge 전에는 protected truth로 표현하지 않는다. ## Completion discipline -각 gap은 표의 authoritative completion evidence가 실제로 존재하고 현재 source/head에 결합될 때만 닫는다. queued/skipped/cancelled/stale check, predecessor-head 결과, 문서 존재, synthetic fixture 또는 model judgement는 완료 증거가 아니다. +각 gap은 표의 authoritative completion evidence가 실제로 존재하고 현재 source/head에 결합될 때만 닫는다. queued/skipped/cancelled/stale check, predecessor-head 결과, 문서 존재, synthetic fixture 또는 model judgement는 완료 증거가 아니다. Noema source의 Apache-2.0 grant, npm package-publication metadata, 제3자 package license evidence는 서로 별도 권위로 유지한다. diff --git a/docs/security/private-vulnerability-reporting-audit.md b/docs/security/private-vulnerability-reporting-audit.md index 49b872361..5ff5ced42 100644 --- a/docs/security/private-vulnerability-reporting-audit.md +++ b/docs/security/private-vulnerability-reporting-audit.md @@ -6,7 +6,7 @@ This runbook explains the repository-owned, read-only evidence probe for GitHub The repository can test whether GitHub currently reports private vulnerability reporting as enabled without granting the audit code permission to change that setting. The audit is deliberately evidence-only: it cannot enable or disable private vulnerability reporting, create a security advisory, change repository permissions, approve a pull request, merge, release, or deploy. -The report is fail-closed unless it can bind the observation to one exact 40-character source revision. Run the probe from a trusted operator environment with outbound HTTPS access to GitHub: +The report is fail-closed unless it can bind the observation to one exact full 40-character SHA-1 or 64-character SHA-256 source revision. Run the probe from a trusted operator environment with outbound HTTPS access to GitHub: ```bash NOEMA_AUDIT_SOURCE_SHA="$(git rev-parse HEAD)" \ diff --git a/scripts/acquisition-readiness-audit.mjs b/scripts/acquisition-readiness-audit.mjs index 01778c78c..d97b8ae7e 100644 --- a/scripts/acquisition-readiness-audit.mjs +++ b/scripts/acquisition-readiness-audit.mjs @@ -440,20 +440,32 @@ function validateLicensingIpEvidence(value) { } const packageJson = readJson("package.json"); + if (!packageJson.ok) { + failures.push("package.json must be readable to determine package distribution status"); + } + const packageDistributionApplies = packageJson.ok && packageJson.value?.private !== true; const packageLicense = packageJson.ok && isNonEmptyString(packageJson.value?.license) ? packageJson.value.license.trim() : ""; const declaredPackageLicense = isNonEmptyString(licensing.package_metadata?.license) ? licensing.package_metadata.license.trim() : ""; - if (!packageLicense) failures.push("package.json license field required"); - if (!declaredPackageLicense) { - failures.push("licensing_ip.package_metadata.license required"); - } else if (packageLicense && declaredPackageLicense !== packageLicense) { - failures.push("package_metadata.license must match package.json license exactly"); + const packageMetadataDeclared = Boolean(packageLicense || declaredPackageLicense); + if (packageDistributionApplies || packageMetadataDeclared) { + if (!packageLicense) failures.push("package.json license field required when package distribution applies or package license metadata is declared"); + if (!declaredPackageLicense) { + failures.push("licensing_ip.package_metadata.license required when package distribution applies or package license metadata is declared"); + } else if (packageLicense && declaredPackageLicense !== packageLicense) { + failures.push("package_metadata.license must match package.json license exactly"); + } } - if (decision && typeof decision === "object" && !Array.isArray(decision)) { + if ( + decision + && typeof decision === "object" + && !Array.isArray(decision) + && (packageDistributionApplies || packageMetadataDeclared) + ) { if ( decision.type === "spdx" && isNonEmptyString(decision.license_expression) @@ -841,4 +853,4 @@ if (!output.passed) { } process.exit(1); } -} +} \ No newline at end of file diff --git a/scripts/lib/private-vulnerability-reporting-audit.mjs b/scripts/lib/private-vulnerability-reporting-audit.mjs index 0909d3f6a..fde183c42 100644 --- a/scripts/lib/private-vulnerability-reporting-audit.mjs +++ b/scripts/lib/private-vulnerability-reporting-audit.mjs @@ -1,4 +1,5 @@ const repositoryPattern = /^ContextualWisdomLab\/[A-Za-z0-9_.-]+$/; +const repositoryPrefix = "ContextualWisdomLab/"; /** * Build the canonical GitHub REST endpoint used to read Noema's private @@ -9,7 +10,14 @@ const repositoryPattern = /^ContextualWisdomLab\/[A-Za-z0-9_.-]+$/; */ export function privateVulnerabilityReportingUrl(repository) { const normalized = String(repository ?? "").trim(); - if (!repositoryPattern.test(normalized)) { + const repositoryName = normalized.startsWith(repositoryPrefix) + ? normalized.slice(repositoryPrefix.length) + : ""; + if ( + !repositoryPattern.test(normalized) + || repositoryName === "." + || repositoryName === ".." + ) { throw new Error("Repository must identify a ContextualWisdomLab repository."); } return `https://api.github.com/repos/${normalized}/private-vulnerability-reporting`; diff --git a/scripts/private-vulnerability-reporting-audit.mjs b/scripts/private-vulnerability-reporting-audit.mjs index d964f27fe..e1d6a7b86 100644 --- a/scripts/private-vulnerability-reporting-audit.mjs +++ b/scripts/private-vulnerability-reporting-audit.mjs @@ -1,7 +1,11 @@ #!/usr/bin/env node -import { mkdirSync, writeFileSync } from "node:fs"; +import { mkdirSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { pathToFileURL } from "node:url"; +import { + assertAcquisitionPrivatePathParents, + writeAcquisitionPrivateFile, +} from "./lib/acquisition-private-output.mjs"; import { evaluatePrivateVulnerabilityReporting, privateVulnerabilityReportingUrl, @@ -12,7 +16,7 @@ const MAX_ERROR_CHARS = 2_000; const MAX_GITHUB_REQUEST_MILLISECONDS = 20_000; const MAX_GITHUB_RESPONSE_BYTES = 16 * 1024; const defaultReportPath = "artifacts/security/private-vulnerability-reporting-audit.json"; -const fullCommitPattern = /^[0-9a-f]{40}$/; +const fullCommitPattern = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/; /** * Normalize an untrusted diagnostic value into a bounded single-line string. @@ -32,7 +36,7 @@ function bound(value, limit = MAX_ERROR_CHARS) { * Resolve the exact source commit that produced the audit evidence. * * @returns {string} Lowercase full Git commit SHA supplied by the trusted execution environment. - * @throws {Error} When source identity is absent or not one full hexadecimal commit SHA. + * @throws {Error} When source identity is absent or not one full hexadecimal SHA-1/SHA-256 commit. */ function sourceRevisionFromEnvironment() { const revision = String( @@ -42,7 +46,7 @@ function sourceRevisionFromEnvironment() { ).trim().toLowerCase(); if (!fullCommitPattern.test(revision)) { throw new Error( - "NOEMA_AUDIT_SOURCE_SHA or GITHUB_SHA must identify the exact 40-character source commit.", + "NOEMA_AUDIT_SOURCE_SHA or GITHUB_SHA must identify an exact 40-character SHA-1 or 64-character SHA-256 source commit.", ); } return revision; @@ -95,11 +99,9 @@ export async function readBoundedJson(response) { } totalBytes += value.byteLength; if (totalBytes > MAX_GITHUB_RESPONSE_BYTES) { - try { - await reader.cancel(); - } catch { - // Preserve the deterministic size-limit failure if cancellation itself fails. - } + void reader.cancel().catch(() => { + // Cancellation is cleanup only; it cannot delay or replace the size-limit failure. + }); throw new Error("GitHub private vulnerability reporting response exceeded the size limit."); } text += decoder.decode(value, { stream: true }); @@ -164,8 +166,13 @@ async function collectPrivateVulnerabilityReporting(repository) { */ function writeReport(path, report) { const absolutePath = resolve(path); - mkdirSync(dirname(absolutePath), { recursive: true }); - writeFileSync(absolutePath, `${JSON.stringify(report, null, 2)}\n`, "utf8"); + assertAcquisitionPrivatePathParents(absolutePath); + mkdirSync(dirname(absolutePath), { recursive: true, mode: 0o700 }); + assertAcquisitionPrivatePathParents(absolutePath); + writeAcquisitionPrivateFile( + absolutePath, + `${JSON.stringify(report, null, 2)}\n`, + ); return absolutePath; } @@ -239,17 +246,39 @@ async function main() { ).trim() || defaultReportPath; let sourceRevision = null; - let report; + let evidenceRepository = "unknown"; + let sourceRevisionError = null; + let repositoryError = null; + try { sourceRevision = sourceRevisionFromEnvironment(); + } catch (error) { + sourceRevisionError = error; + } + + try { + privateVulnerabilityReportingUrl(repository); + evidenceRepository = repository; + } catch (error) { + repositoryError = error; + } + + let report; + try { + if (sourceRevisionError) { + throw sourceRevisionError; + } + if (repositoryError) { + throw repositoryError; + } const payload = await collectPrivateVulnerabilityReporting(repository); report = buildReport( - repository, + evidenceRepository, evaluatePrivateVulnerabilityReporting(payload), sourceRevision, ); } catch (error) { - report = buildCollectionFailure(repository || "unknown", error, sourceRevision); + report = buildCollectionFailure(evidenceRepository, error, sourceRevision); } const absoluteReportPath = writeReport(reportPath, report); diff --git a/test/acquisition-source-only-license.test.ts b/test/acquisition-source-only-license.test.ts new file mode 100644 index 000000000..d7d8093f3 --- /dev/null +++ b/test/acquisition-source-only-license.test.ts @@ -0,0 +1,199 @@ +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +const temporaryRoots: string[] = []; + +function writeFixture(root: string, relativePath: string, content: string): string { + const path = join(root, relativePath); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, content, "utf8"); + return path; +} + +function sha256(value: string): string { + return createHash("sha256").update(value).digest("hex"); +} + +function digestArtifact(root: string, relativePath: string, content: string) { + writeFixture(root, relativePath, content); + return { path: relativePath, sha256: sha256(content) }; +} + +function writeRequiredDocs(root: string): void { + writeFixture( + root, + "docs/acquisition-readiness-2b.md", + "NOEMA-GOAL-ACQUISITION-2B-2026-07-02\nKRW 2,000,000,000\nRevenue_PASS\nTransfer_PASS\n", + ); + writeFixture( + root, + "docs/buyer-due-diligence-index.md", + "npm run acquisition:audit\nartifacts/acquisition/revenue-evidence.json\nartifacts/acquisition/transfer-evidence.json\n", + ); + writeFixture( + root, + "docs/library-boundary-decision.md", + "현재는 submodule을 만들지 않는다\nnpm workspaces\nSplit Triggers\n", + ); + writeFixture( + root, + "scripts/acquisition-data-room-manifest.mjs", + "// finalGatePassed data-room-manifest.json release-publication-receipt\n", + ); + writeFixture(root, "docs/saleable-program-goal-registry.md", "NOEMA-GOAL-SALEABLE-2026-07-02\n"); + writeFixture(root, "docs/pricing-draft.md", "pricing draft\n"); + writeFixture(root, "docs/terms-draft.md", "terms draft\n"); + writeFixture(root, "docs/sla-and-support.md", "support draft\n"); +} + +function writeSourceOnlyTransferEvidence(root: string, packagePrivate = true): string { + const licenseBytes = "Apache License 2.0 reviewed fixture.\n"; + writeFixture(root, "LICENSE", licenseBytes); + writeFixture( + root, + "package.json", + `${JSON.stringify({ name: "noema", private: packagePrivate }, null, 2)}\n`, + ); + + const artifactRights = `${JSON.stringify({ + schema_version: 1, + repository: "ContextualWisdomLab/noema", + tag: "v0.1.0", + commit_sha: "a".repeat(40), + artifacts: [ + { + artifact_kind: "source_archive", + artifact_identity: `noema-${"a".repeat(40)}.tar.gz`, + }, + ], + }, null, 2)}\n`; + + const licensingIp = { + owner_legal_decision: { + type: "spdx", + license_expression: "Apache-2.0", + evidence: ["legal/outbound-rights-decision.pdf"], + }, + repository_rights: { + path: "LICENSE", + sha256: sha256(licenseBytes), + }, + release_rights: { + tag: "v0.1.0", + commit_sha: "a".repeat(40), + sbom: digestArtifact(root, "artifacts/release/noema.cdx.json", "{\"bomFormat\":\"CycloneDX\"}\n"), + dependency_license_inventory: digestArtifact( + root, + "artifacts/release/dependency-licenses.json", + "{\"dependencies\":[]}\n", + ), + notice: digestArtifact(root, "artifacts/release/NOTICE.txt", "Reviewed third-party notices.\n"), + provenance: digestArtifact(root, "artifacts/release/provenance.sigstore.json", "{\"verified\":true}\n"), + artifact_rights_metadata: digestArtifact( + root, + "artifacts/release/artifact-rights-metadata.json", + artifactRights, + ), + }, + contributor_ip: { + ownership_evidence: ["legal/contributor-ownership-register.pdf"], + assignment_evidence: ["legal/ip-assignment-register.pdf"], + }, + }; + + return writeFixture( + root, + "artifacts/acquisition/transfer-evidence.json", + `${JSON.stringify({ + owner: "Acquisition counsel", + source_documents: ["legal/review-record.pdf"], + updated_at: new Date().toISOString(), + license_review: "pass", + third_party_review: "pass", + github_app_transfer_plan: "pass", + cloudflare_transfer_plan: "pass", + secrets_rotation_plan: "pass", + owner_transfer_plan: "pass", + privacy_review: "pass", + licensing_ip: licensingIp, + }, null, 2)}\n`, + ); +} + +function runReportOnlyAudit(root: string, transferEvidencePath: string) { + const outputDir = join(root, "audit-output"); + const script = resolve("scripts/acquisition-readiness-audit.mjs"); + const inheritedEnvironment = Object.fromEntries( + Object.entries(process.env).filter(([key]) => !key.startsWith("NOEMA_")), + ); + const result = spawnSync(process.execPath, [script], { + cwd: root, + encoding: "utf8", + env: { + ...inheritedEnvironment, + NOEMA_AUDIT_REPORT_ONLY: "1", + NOEMA_TRANSFER_EVIDENCE_PATH: transferEvidencePath, + NOEMA_ACQUISITION_AUDIT_OUTPUT_DIR: outputDir, + }, + }); + const auditPath = join(outputDir, "acquisition-audit.json"); + if (!existsSync(auditPath)) return { result, transferCheck: undefined }; + const audit = JSON.parse(readFileSync(auditPath, "utf8")); + const transferCheck = audit.checks.find( + (check: { name?: string }) => check.name === "transfer evidence pass", + ); + return { result, transferCheck }; +} + +afterEach(() => { + while (temporaryRoots.length > 0) { + const root = temporaryRoots.pop(); + if (root) rmSync(root, { recursive: true, force: true }); + } +}); + +describe("source-only repository licensing", () => { + it("accepts a private non-published npm manifest when repository source rights are complete", () => { + const root = mkdtempSync(join(tmpdir(), "noema-source-only-license-")); + temporaryRoots.push(root); + writeRequiredDocs(root); + const transferEvidencePath = writeSourceOnlyTransferEvidence(root); + + const { result, transferCheck } = runReportOnlyAudit(root, transferEvidencePath); + + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(transferCheck).toBeDefined(); + expect(transferCheck.pass).toBe(true); + expect(transferCheck.details.licensingIpFailures).toEqual([]); + }); + + it("still requires package license metadata when the package is distributable", () => { + const root = mkdtempSync(join(tmpdir(), "noema-distributable-package-license-")); + temporaryRoots.push(root); + writeRequiredDocs(root); + const transferEvidencePath = writeSourceOnlyTransferEvidence(root, false); + + const { result, transferCheck } = runReportOnlyAudit(root, transferEvidencePath); + + expect(result.status, result.stderr || result.stdout).toBe(0); + expect(transferCheck).toBeDefined(); + expect(transferCheck.pass).toBe(false); + expect(transferCheck.details.licensingIpFailures).toEqual( + expect.arrayContaining([ + "package.json license field required when package distribution applies or package license metadata is declared", + "licensing_ip.package_metadata.license required when package distribution applies or package license metadata is declared", + ]), + ); + }); +}); diff --git a/test/private-vulnerability-reporting-audit.test.ts b/test/private-vulnerability-reporting-audit.test.ts index 3d6978844..58c90c59a 100644 --- a/test/private-vulnerability-reporting-audit.test.ts +++ b/test/private-vulnerability-reporting-audit.test.ts @@ -1,5 +1,16 @@ -import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { readBoundedJson } from "../scripts/private-vulnerability-reporting-audit.mjs"; import { evaluatePrivateVulnerabilityReporting, privateVulnerabilityReportingUrl, @@ -39,6 +50,39 @@ describe("private vulnerability reporting operational audit", () => { expect(evaluatePrivateVulnerabilityReporting(null).status).toBe("FAIL"); }); + it("retains SHA-256 Git source identity in fail-closed audit evidence", () => { + const root = mkdtempSync(join(tmpdir(), "noema-private-reporting-sha256-")); + const reportPath = join(root, "audit.json"); + const sourceRevision = "a".repeat(64); + try { + const result = spawnSync( + process.execPath, + ["scripts/private-vulnerability-reporting-audit.mjs"], + { + cwd: process.cwd(), + env: { + ...process.env, + NOEMA_AUDIT_SOURCE_SHA: sourceRevision, + NOEMA_AUDIT_REPOSITORY: "OtherOrg/noema", + NOEMA_PRIVATE_VULNERABILITY_REPORTING_AUDIT_PATH: reportPath, + }, + encoding: "utf8", + timeout: 5_000, + }, + ); + + expect(result.status).toBe(1); + const report = JSON.parse(readFileSync(reportPath, "utf8")); + expect(report.source_revision).toBe(sourceRevision); + expect(report.status).toBe("FAIL"); + expect(report.failures[0]?.detail).toContain( + "Repository must identify a ContextualWisdomLab repository.", + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + it("binds collection to the expected public GitHub repository endpoint", () => { expect(privateVulnerabilityReportingUrl("ContextualWisdomLab/noema")).toBe( "https://api.github.com/repos/ContextualWisdomLab/noema/private-vulnerability-reporting", @@ -69,6 +113,102 @@ describe("private vulnerability reporting operational audit", () => { expect(script).toContain("does not prove notification routing or an end-to-end private-report exercise"); }); + it("does not let stalled stream cancellation suppress the size-limit failure", async () => { + const response = new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array((16 * 1024) + 1)); + }, + cancel() { + return new Promise(() => {}); + }, + }), + { headers: { "content-type": "application/json" } }, + ); + + const outcome = await Promise.race([ + readBoundedJson(response).then( + () => ({ kind: "resolved" as const }), + (error) => ({ + kind: "rejected" as const, + message: String(error?.message || error), + }), + ), + new Promise<{ kind: "timeout" }>((resolve) => { + setTimeout(() => resolve({ kind: "timeout" }), 250); + }), + ]); + + expect(outcome).toEqual({ + kind: "rejected", + message: "GitHub private vulnerability reporting response exceeded the size limit.", + }); + }); + + it("does not follow a symlinked retained-report leaf into unrelated evidence", () => { + const root = mkdtempSync(join(tmpdir(), "noema-private-reporting-")); + try { + const reportDirectory = join(root, "reports"); + mkdirSync(reportDirectory); + const victimPath = join(root, "victim.json"); + const reportPath = join(reportDirectory, "audit.json"); + writeFileSync(victimPath, "buyer-evidence-must-survive\n", "utf8"); + symlinkSync(victimPath, reportPath); + + const result = spawnSync( + process.execPath, + ["scripts/private-vulnerability-reporting-audit.mjs"], + { + cwd: process.cwd(), + env: { + ...process.env, + NOEMA_AUDIT_SOURCE_SHA: "not-a-commit", + NOEMA_PRIVATE_VULNERABILITY_REPORTING_AUDIT_PATH: reportPath, + }, + encoding: "utf8", + timeout: 5_000, + }, + ); + + expect(result.status).not.toBe(0); + expect(readFileSync(victimPath, "utf8")).toBe("buyer-evidence-must-survive\n"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("does not traverse a symlinked retained-report parent directory", () => { + const root = mkdtempSync(join(tmpdir(), "noema-private-reporting-parent-")); + try { + const outsideDirectory = join(root, "outside"); + mkdirSync(outsideDirectory); + const linkedDirectory = join(root, "reports"); + symlinkSync(outsideDirectory, linkedDirectory, "dir"); + const reportPath = join(linkedDirectory, "audit.json"); + const outsideReportPath = join(outsideDirectory, "audit.json"); + + const result = spawnSync( + process.execPath, + ["scripts/private-vulnerability-reporting-audit.mjs"], + { + cwd: process.cwd(), + env: { + ...process.env, + NOEMA_AUDIT_SOURCE_SHA: "not-a-commit", + NOEMA_PRIVATE_VULNERABILITY_REPORTING_AUDIT_PATH: reportPath, + }, + encoding: "utf8", + timeout: 5_000, + }, + ); + + expect(result.status).not.toBe(0); + expect(() => readFileSync(outsideReportPath, "utf8")).toThrow(); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + it("keeps every named audit helper adjacent to a JSDoc contract", () => { const script = readFileSync("scripts/private-vulnerability-reporting-audit.mjs", "utf8"); const contracts = [ diff --git a/test/private-vulnerability-reporting-invalid-repository-evidence.test.ts b/test/private-vulnerability-reporting-invalid-repository-evidence.test.ts new file mode 100644 index 000000000..cc2cb6090 --- /dev/null +++ b/test/private-vulnerability-reporting-invalid-repository-evidence.test.ts @@ -0,0 +1,42 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +describe("private vulnerability reporting failure evidence repository authority", () => { + it("does not retain a deceptive invalid repository identity", () => { + const root = mkdtempSync(join(tmpdir(), "noema-private-reporting-invalid-repository-")); + const reportPath = join(root, "audit.json"); + const deceptiveRepository = "ContextualWisdomLab/noema\u202Egpj"; + + try { + const result = spawnSync( + process.execPath, + ["scripts/private-vulnerability-reporting-audit.mjs"], + { + cwd: process.cwd(), + env: { + ...process.env, + NOEMA_AUDIT_SOURCE_SHA: "a".repeat(40), + NOEMA_AUDIT_REPOSITORY: deceptiveRepository, + NOEMA_PRIVATE_VULNERABILITY_REPORTING_AUDIT_PATH: reportPath, + }, + encoding: "utf8", + timeout: 5_000, + }, + ); + + expect(result.status).toBe(1); + const report = JSON.parse(readFileSync(reportPath, "utf8")); + expect(report.status).toBe("FAIL"); + expect(report.repository).toBe("unknown"); + expect(report.failures[0]?.detail).toContain( + "Repository must identify a ContextualWisdomLab repository.", + ); + expect(JSON.stringify(report)).not.toContain("\u202E"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/test/private-vulnerability-reporting-invalid-source-evidence.test.ts b/test/private-vulnerability-reporting-invalid-source-evidence.test.ts new file mode 100644 index 000000000..8398aa054 --- /dev/null +++ b/test/private-vulnerability-reporting-invalid-source-evidence.test.ts @@ -0,0 +1,41 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +describe("private vulnerability reporting source failure evidence repository authority", () => { + it("retains a separately validated repository when source identity is invalid", () => { + const root = mkdtempSync(join(tmpdir(), "noema-private-reporting-invalid-source-")); + const reportPath = join(root, "audit.json"); + + try { + const result = spawnSync( + process.execPath, + ["scripts/private-vulnerability-reporting-audit.mjs"], + { + cwd: process.cwd(), + env: { + ...process.env, + NOEMA_AUDIT_SOURCE_SHA: "a".repeat(39), + NOEMA_AUDIT_REPOSITORY: "ContextualWisdomLab/noema", + NOEMA_PRIVATE_VULNERABILITY_REPORTING_AUDIT_PATH: reportPath, + }, + encoding: "utf8", + timeout: 5_000, + }, + ); + + expect(result.status).toBe(1); + const report = JSON.parse(readFileSync(reportPath, "utf8")); + expect(report.status).toBe("FAIL"); + expect(report.repository).toBe("ContextualWisdomLab/noema"); + expect(report.source_revision).toBeNull(); + expect(report.failures[0]?.detail).toContain( + "must identify an exact 40-character SHA-1 or 64-character SHA-256 source commit", + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/test/private-vulnerability-reporting-repository-authority.test.ts b/test/private-vulnerability-reporting-repository-authority.test.ts new file mode 100644 index 000000000..f5e80248b --- /dev/null +++ b/test/private-vulnerability-reporting-repository-authority.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { privateVulnerabilityReportingUrl } from "../scripts/lib/private-vulnerability-reporting-audit.mjs"; + +describe("private vulnerability reporting repository authority", () => { + it.each([ + "ContextualWisdomLab/.", + "ContextualWisdomLab/..", + ])("rejects dot-segment repository names before constructing the GitHub endpoint: %s", (repository) => { + expect(() => privateVulnerabilityReportingUrl(repository)).toThrow( + "Repository must identify a ContextualWisdomLab repository.", + ); + }); +}); diff --git a/test/readme-customer-facing.test.ts b/test/readme-customer-facing.test.ts index 2de4f62fe..23dde3a3c 100644 --- a/test/readme-customer-facing.test.ts +++ b/test/readme-customer-facing.test.ts @@ -10,12 +10,10 @@ describe("README stays customer/operator facing", () => { const contributing = readText("CONTRIBUTING.md"); const procedure = readText("docs/development/contributor-and-agent-procedure.md"); - it("leads with product, standalone run, host call, and operator config", () => { + it("leads with product value, supported surfaces, onboarding, and trust boundaries", () => { for (const requiredText of [ - "leaf product", - "따로 또 같이", - "naruon", - "gyeot", + "Evidence-producing credential and maintenance control plane", + "What Noema does", "/health", "/ready", "/exchange", @@ -27,16 +25,20 @@ describe("README stays customer/operator facing", () => { "NOEMA_LLM_API_URL", "NOEMA_LLM_API_KEY", "contextual-orchestrator", - "hourly-product-development", - "NOEMA_RATE_LIMIT_PER_MINUTE", + "GitHub App permissions", + "Security model", + "Verify the repository", + "Apache License 2.0", ]) { expect(readme).toContain(requiredText); } }); - it("does not keep bot-manual leftover in README", () => { + it("does not keep internal topology or bot-manual leftovers in README", () => { expect(readme).not.toContain("GITHUB_APP_SLUG"); for (const leftover of [ + "leaf product", + "따로 또 같이", "CloudAgent", "OpenCode 1.17.13", "OpenCode session", @@ -44,20 +46,18 @@ describe("README stays customer/operator facing", () => { "PR stacking", "stacked PR", "do-not-merge", - "Do not merge", "writer/agent", "writer and agent", "exact-head CI", - "Do not fabricate", "proposal-only OpenCode", ]) { - expect(readme, `README must not contain bot-manual leftover: ${leftover}`).not.toContain( + expect(readme, `README must not contain internal/bot-manual leftover: ${leftover}`).not.toContain( leftover, ); } }); - it("relocates contributor and agent procedure to internal docs", () => { + it("keeps contributor and agent procedure in internal documentation", () => { expect(contributing).toContain("docs/development/contributor-and-agent-procedure.md"); expect(contributing).toContain("hourly-product-development.yml"); expect(contributing).toContain("exact-head"); diff --git a/test/workflow-runner-selector.test.ts b/test/workflow-runner-selector.test.ts new file mode 100644 index 000000000..f3bbacaa1 --- /dev/null +++ b/test/workflow-runner-selector.test.ts @@ -0,0 +1,32 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +const requiredRunnerBackedWorkflows = [ + ".github/workflows/ci.yml", + ".github/workflows/reviewer-ci.yml", + ".github/workflows/patch-validator-image.yml", +] as const; + +const runsOnSelector = /^\s*runs-on:\s*(["']?)([^\s"']+)\1\s*$/m; + +describe("required workflow runner selector", () => { + it.each(requiredRunnerBackedWorkflows)( + "%s uses the explicit supported Ubuntu runner image", + (workflowPath) => { + const workflow = readFileSync(workflowPath, "utf8"); + const match = workflow.match(runsOnSelector); + + expect(match?.[2]).toBe("ubuntu-24.04"); + expect(match?.[2]).not.toBe("ubuntu-latest"); + }, + ); + + it.each([ + "runs-on: ubuntu-latest", + 'runs-on: "ubuntu-latest"', + "runs-on: 'ubuntu-latest'", + ])("rejects floating runner selector syntax: %s", (workflowLine) => { + expect(workflowLine.match(runsOnSelector)?.[2]).toBe("ubuntu-latest"); + }); +});