From 3c8bc785bf0b8453d77b32357fd7058d981d8590 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Tue, 4 Aug 2026 09:26:33 +0200 Subject: [PATCH 1/6] chore(harness): lock aspire cli hardening design --- .../context-pack.md | 70 +++++++++ .../drift.md | 23 +++ .../plan-eval.md | 8 ++ .../plan.md | 133 ++++++++++++++++++ .../research.md | 34 +++++ .../supervisor.md | 25 ++++ .../worklog.md | 103 ++++++++++++++ 7 files changed, 396 insertions(+) create mode 100644 .llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md create mode 100644 .llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md create mode 100644 .llm/runs/feat-openapi-mcp-manifest-cli--1133/plan-eval.md create mode 100644 .llm/runs/feat-openapi-mcp-manifest-cli--1133/plan.md create mode 100644 .llm/runs/feat-openapi-mcp-manifest-cli--1133/research.md create mode 100644 .llm/runs/feat-openapi-mcp-manifest-cli--1133/supervisor.md create mode 100644 .llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md new file mode 100644 index 0000000000..8387158ac0 --- /dev/null +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md @@ -0,0 +1,70 @@ +# Context Pack: Aspire CLI adapter hardening + +## Run Metadata + +| Field | Value | +| --- | --- | +| Run ID | `feat-openapi-mcp-manifest-cli--1133` | +| Branch | `feat/openapi-mcp-manifest-cli` | +| Current phase | `implement` | +| Archetype | `2 - Integration` | +| Scope overlays | `none` | + +## Current State + +Branch is fast-forwarded to main after S5. Research and design are locked for the F1(b) adapter +hardening. Baseline verification owns the AppHost slot; no expensive gate has started here. + +## Completed + +- Required skills/docs, issue, RFC §F1, P1 verdict, doctrine, gate matrix, and real Aspire output read. +- S6 checked: open with no PR, so directory fixture is current E2E fallback. +- Composed/not-local PLAN-EVAL ruling recorded. + +## In Progress + +- Implement pre/post `aspire ps` identity binding, project-root validation, drift-tolerant parsing, + explicit failures, and fixtures. + +## Next Steps + +1. Implement adapter and focused tests. +2. Run Archetype-2/package/JSR gates and review the slice. +3. Re-check S6 and AppHost slot, then run serialized scaffold runtime evidence. +4. Push explicit refspec, maintain draft PR phase trail, and hand to composed evaluation. + +## Key Decisions + +| Decision | Source | Notes | +| --- | --- | --- | +| F1(b) only | P1/RFC/owner | No template manifest emission. | +| Exact path + stable AppHost PID | real Aspire `ps` | CLI adapter's observable run binding. | +| Foreign resource rejects source | doctrine/S-8 | Never partially trust foreign describe output. | + +## Files Changed + +| Path | Status | Notes | +| --- | --- | --- | +| `.llm/runs/feat-openapi-mcp-manifest-cli--1133/*` | new | Harness bootstrap and locked design. | + +## Gates + +| Gate family | Current status | Evidence | +| --- | --- | --- | +| Static | pending | implementation not started | +| Fitness | pending | implementation not started | +| Runtime | queued | baseline owns AppHost slot | +| Consumer | queued | directory fixture unless S6 lands | + +## Open Questions + +- Will S6 land before the serialized runtime gate? + +## Drift and Debt + +- Drift: F1(b) rescope and composed PLAN-EVAL are recorded. +- Debt: none created. + +## Commits + +- See the draft PR's commit list + per-slice PR comments. diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md new file mode 100644 index 0000000000..4562db9f12 --- /dev/null +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md @@ -0,0 +1,23 @@ +# Drift Log: Aspire CLI adapter hardening + +## 2026-08-04 — F1(b) re-scope replaces manifest emission + +- **What:** Issue #1133's original manifest-template deliverable is replaced by production hardening + of the `aspire-cli` endpoint source. +- **Source:** RFC #1123 §F1, P1 verdict, owner/orchestrator comment, staged brief. +- **Expected:** Post-allocation manifest template emission if P1 passed. +- **Actual:** P1 `FAIL` selects qualified F1(b); S5's CLI adapter is primary and this slice extends it. +- **Severity:** significant +- **Action:** rescope +- **Evidence:** `.llm/runs/test-openapi-mcp-wave0-proofs--wave0/proofs/P1-verdict.md` + +## 2026-08-04 — formal PLAN-EVAL composed at milestone level + +- **What:** No local formal PLAN-EVAL is launched. +- **Source:** Milestone-run evaluator protocol and orchestrator ruling D6 in the owner brief. +- **Expected:** Ordinary single-run harness would use a separate local PLAN-EVAL. +- **Actual:** `plan-eval.md` records `COMPOSED_NOT_LOCAL`; evaluation occurs via separate composed surfaces. +- **Severity:** minor +- **Action:** accept +- **Evidence:** `.llm/harness/workflow/milestone-run.md` + diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/plan-eval.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/plan-eval.md new file mode 100644 index 0000000000..156852435e --- /dev/null +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/plan-eval.md @@ -0,0 +1,8 @@ +# PLAN-EVAL — feat-openapi-mcp-manifest-cli--1133 + +`COMPOSED_NOT_LOCAL` + +Per `milestone-run.md` evaluator protocol and orchestrator ruling D6, this delegated milestone PR +does not spawn a local formal PLAN-EVAL. Evaluation is composed at draft-to-ready through the +separate CI/reviewer surfaces. The implementation supervisor does not self-certify the result. + diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/plan.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/plan.md new file mode 100644 index 0000000000..a8c72d0aa0 --- /dev/null +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/plan.md @@ -0,0 +1,133 @@ +# Plan: production-harden the Aspire CLI endpoint source + +## Run Metadata + +| Field | Value | +| --- | --- | +| Run ID | `feat-openapi-mcp-manifest-cli--1133` | +| Branch | `feat/openapi-mcp-manifest-cli` | +| Phase | `plan` | +| Target | `packages/mcp` Aspire CLI endpoint adapter + scaffold runtime evidence | +| Archetype | `2 - Integration` | +| Scope overlays | `none` | + +## Archetype + +Archetype 2 governs: a small consumed port already has four source adapters; this slice hardens the +external Aspire CLI adapter and its failure boundary without adding runtime lifecycle ownership. + +## Current Doctrine Verdict + +Keep effects at the adapter edge, preserve the port-owned finite vocabulary, and keep application +composition dependent on the port rather than CLI details. No new debt is planned. + +## Axioms in Play + +| Axiom | Why it matters | +| --- | --- | +| A1 | Failure and identity vocabulary remains explicit in the published port contract. | +| A7 | Use `Deno.Command`, URL/path primitives, and injected seams; no process wrapper dependency. | +| A11 | Extend the named `EndpointSource` axis rather than fork discovery. | +| A13 | CLI spawn, parse, restart races, and foreign identity are explicit crash boundaries. | +| A14 | Fixtures and runtime evidence prove negative cases fire. | + +## Goal + +Make `aspire-cli` a trustworthy primary live endpoint source: tolerate benign output drift while +explicitly failing CLI absence, non-zero exit, torn output, foreign project resources, and AppHost +restart races. Prove live scaffold ports resolve through the directory (or S6 tool if landed). + +## Scope + +- Query `aspire ps` around `aspire describe` and bind the result to exact AppHost path/process run. +- Real-path bind AppHost and executable resource working directories to `projectRoot`. +- Parse documented field aliases and banner-prefixed balanced JSON without accepting torn payloads. +- Add deterministic adapter fixtures for drift and every failure/identity state. +- Update package docs and generated publish assets if the public behavior description changes. +- Queue one `scaffold.runtime` run behind the current baseline owner. + +## Non-Scope + +- No endpoint manifest template emission (F1(b)). +- No S6 read-tool implementation or registry wiring. +- No changes to source precedence, probing, OpenAPI projection, or service templates. +- No AppHost cleanup belonging to the baseline verification. + +## Hidden Scope + +- Preserve cancellation across all CLI invocations. +- Bound failure reasons so CLI output cannot become an unbounded MCP row. +- Regenerate package publish assets if README content changes. +- Re-check S6 and expensive-gate ownership immediately before runtime validation. + +## Locked Decisions + +| ID | Decision | Rationale | +| --- | --- | --- | +| D1 | Treat exact real `appHostPath` + stable `appHostPid` as the CLI adapter run binding. | These are the run identity facts exposed by `aspire ps`; the manifest UUID is not observable through CLI. | +| D2 | Read `ps`, then `describe`, then `ps`; mismatch is `run_id_mismatch`. | Prevents stale/torn describe output from reading as current after restart. | +| D3 | Require candidate executable `workDir` to resolve inside real `projectRoot`; foreign candidates fail the whole source. | Partial trust would let a foreign endpoint appear live. | +| D4 | Accept casing aliases and balanced banner/trailer JSON, but reject incomplete JSON and missing required structures. | Tolerates version/format drift without converting corruption into health. | +| D5 | Reuse existing explicit source failure rows; add only the finite codes needed for CLI identity ambiguity. | Keeps P3-style visible degraded data at the source boundary. | + +## Open-Decision Sweep + +| Decision | Status | Notes | +| --- | --- | --- | +| E2E call path | safe to defer until gate | Use `list_api_services` only if S6 merges; otherwise fixture `directory.list()`. | +| AppHost slot timing | safe to defer until gate | Serialized behind baseline; no local start while `aspire ps` is non-empty for that owner. | + +## Risk Register + +| Risk | Mitigation | +| --- | --- | +| `ps` format varies | Parse array or common envelope aliases; fixture both. | +| PID changes between calls | Pre/post identity equality gate. | +| Some non-executable URL resource lacks workDir | Only accept endpoint candidates with a trustworthy project-root binding; surface failure instead of guessing. | +| S6 lands during work | Rebase before runtime gate and switch evidence to the public tool path. | + +## Anti-Patterns to Resolve or Avoid + +| AP | Status | Plan | +| --- | --- | --- | +| AP-2 | risk | Keep parsing/domain value-add in adapter; do not wrap `Deno.Command` generically. | +| AP-9 | risk | Add focused parsing functions only where real CLI variants require them. | +| AP-19 | existing documented | Preserve README `--allow-run` declaration. | +| AP-25 | compliant | Process effect remains inside infrastructure adapter. | + +## Fitness Gates + +| Gate | Required | Expected evidence | +| --- | --- | --- | +| F-3 layering | yes | `deno task arch:check` | +| F-5 public surface/docs | yes | `deno doc --lint` full package exports | +| F-6 publishability | yes | package `deno publish --dry-run` | +| Archetype-2 full column | yes | scoped static, unit/integration, contract, fault, consumer, quality gates | + +## Arch-Debt Implications + +| Entry | Action | Notes | +| --- | --- | --- | +| `.llm/harness/debt/arch-debt.md` | none expected | Record only if the real CLI cannot supply the locked identity proof. | + +## Validation Plan + +| Order | Gate | Command or check | Expected result | +| --- | --- | --- | --- | +| 1 | focused tests | `deno test -A packages/mcp/tests/service-endpoint-sources_test.ts packages/mcp/tests/service-endpoint-directory_test.ts` | all pass | +| 2 | scoped static | run-deno check/lint/fmt wrappers over `packages/mcp` | pass | +| 3 | package tests | package test task | pass | +| 4 | doctrine quality | `deno task quality:scan` + `deno task arch:check` | pass | +| 5 | JSR | full export doc lint + package dry-run | pass, zero slow types | +| 6 | consumer runtime | serialized `scaffold.runtime`; S6 tool or directory fixture | live allocated port resolved through `aspire-cli` | + +## Dependencies + +- S5/#1131 is present on main. +- S6/#1132 is optional for the E2E presentation edge and currently open. +- Baseline verification currently owns the single AppHost slot. + +## Drift Watch + +- Any need for template emission, a new process, a second port, or non-CLI identity carrier is a rescope. + diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/research.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/research.md new file mode 100644 index 0000000000..4d4255b602 --- /dev/null +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/research.md @@ -0,0 +1,34 @@ +# Research — feat-openapi-mcp-manifest-cli--1133 + +## Re-baseline + +- Carried-in source: `/home/codex/ns005s7-brief.md`, issue #1133, RFC #1123 §F1, and P1 verdict. +- Re-derived against `main` @ `f7558aa1c` on 2026-08-04. +- S5 landed on main in #1194 and already supplies the Aspire CLI adapter, endpoint-source port, + precedence, probe isolation, and explicit source outcomes. This run extends that adapter. +- P1 is `FAIL`; qualified F1(b) is authoritative. No template manifest emission is in scope. +- S6/#1132 remains open with no PR, so the expensive evidence must fixture the directory call unless + S6 merges before the gate is run. + +## Findings + +| # | Finding | How to verify | +| --- | --- | --- | +| 1 | Current adapter invokes `aspire describe` directly and trusts any parseable `resources[]`. | `packages/mcp/src/infrastructure/service-endpoints/aspire-cli-endpoint-source.ts` | +| 2 | Aspire 13.4.6 `ps --format Json` identifies a run by exact `appHostPath` and `appHostPid`. | Read-only `aspire ps --format Json` captured 2026-08-04. | +| 3 | Real describe resources expose `properties.executable.workDir`; executable service candidates can be bound to `projectRoot`. | Read-only describe of the baseline verification AppHost. | +| 4 | The live AppHost slot is occupied by baseline verification; no runtime gate may start yet. | `aspire ps --format Json` showed three AppHosts under `ns005-baseline`. | +| 5 | Banner noise and field-name drift are plausible across CLI versions; torn/trailing output must never be silently accepted. | Existing 13.4.6 fixture plus F1(b) brief. | + +## jsr-audit surface scan (package/plugin waves) + +- Surface scanned: `packages/mcp/deno.json`, root `mod.ts`, and exported Aspire adapter types. +- The existing public types are documented. This slice should avoid adding a new export; any added + option stays on the already-exported options interface and receives JSDoc. +- Full `deno doc --lint` and package dry-run are required after implementation. + +## Open questions + +- S6 may merge before the serialized runtime gate. Re-check immediately before running it; otherwise + exercise `createServiceEndpointDirectory().list()` directly in the scaffold fixture. + diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/supervisor.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/supervisor.md new file mode 100644 index 0000000000..a3e3e1c495 --- /dev/null +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/supervisor.md @@ -0,0 +1,25 @@ +# Supervisor Identity — feat-openapi-mcp-manifest-cli--1133 + +| Field | Value | +| --- | --- | +| Model | Codex GPT-5 | +| Session | Codex API session (thread identifier not exposed) | +| Host | Linux / `/home/codex` | +| Checkout | `/home/codex/repos/ns005-s7` | +| Worktree | `/home/codex/repos/ns005-s7` | +| Branch | `feat/openapi-mcp-manifest-cli` | +| Baseline | `f7558aa1c4e06f076114d924c7324feddf554e45` (`origin/main`, 2026-08-04) | +| Run ID | `feat-openapi-mcp-manifest-cli--1133` | + +## Routes in force + +| Task lane | Provider / model / effort | Role in this run | +| --- | --- | --- | +| implementation supervisor | Codex GPT-5 | research, plan, implementation, gate evidence, PR handoff | +| composed evaluator | milestone-run CI/reviewer composition | separate implementation evaluation at draft-to-ready | + +## Recorded lane/eval overrides + +- Owner/orchestrator ruling D6 applies: no local formal PLAN-EVAL for this delegated milestone PR. + `plan-eval.md` records `COMPOSED_NOT_LOCAL`; plan and implementation proceed in this run. + diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md new file mode 100644 index 0000000000..186a798a12 --- /dev/null +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md @@ -0,0 +1,103 @@ +# Worklog: Aspire CLI adapter production hardening + +## Run Metadata + +| Field | Value | +| --- | --- | +| Run ID | `feat-openapi-mcp-manifest-cli--1133` | +| Branch | `feat/openapi-mcp-manifest-cli` | +| Archetype | `2 - Integration` | +| Scope overlays | `none` | + +## Design + +### Public Surface + +- Existing `AspireCliEndpointSource` and `AspireCliEndpointSourceOptions`; no new entrypoint. +- Existing `SourceOutcome` exposes every failure as data. + +### Domain Vocabulary + +- `appHostPath` — exact real path selecting one AppHost. +- `appHostPid` — CLI-visible run identity, stable across one describe snapshot. +- `projectRoot` — real-path ownership boundary for AppHost and resource working directories. + +### Ports + +- `EndpointSourcePort` — unchanged consumed contract. +- `AspireCliCommand` — existing injected spawn boundary, reused for `ps` and `describe`. + +### Constants + +- Existing `SOURCE_FAILURE_CODES`; extend only for a finite, machine-readable CLI identity failure. + +### Commit Slices + +| # | Slice | Gate | Files | +| --- | --- | --- | --- | +| 1 | Harness bootstrap and locked design | artifact review | run dir | +| 2 | Identity-bound drift-tolerant CLI adapter + fixtures | focused MCP tests + static/fitness gates | adapter, port, tests, docs, run dir | +| 3 | Serialized scaffold runtime proof and handoff | `scaffold.runtime` plus separate composed evaluation | E2E evidence, run dir, PR surface | + +### Deferred Scope + +- `list_api_services` tool wiring remains S6 unless it merges before the runtime gate. +- Manifest emission is rejected by qualified F1(b). + +### Contributor Path + +Add a future Aspire output variant by extending the adapter's named field readers and adding one +fixture that proves both accepted drift and rejection of an adjacent torn/ambiguous shape. + +## Progress Log + +| Time | Slice | Step | Notes | +| --- | --- | --- | --- | +| 2026-08-04 | 1 | bootstrap | Read brief, five named skills, tooling/JSR guidance, #1133, RFC §F1, P1 verdict, doctrine, matrix, and real CLI output. | +| 2026-08-04 | 1 | serialization | `aspire ps` showed baseline-owned AppHosts; runtime gate queued. | + +## Decisions + +| Decision | Reason | Source | +| --- | --- | --- | +| Qualified F1(b), no template emission | P1 locked verdict | #1123 §F1 / P1 verdict / owner brief | +| CLI run binding is exact AppHost path + stable PID | Facts exposed by real Aspire 13.4.6 `ps` | research D1-D3 | +| PLAN-EVAL is composed/not-local | Milestone evaluator rule | owner brief / milestone-run D6 | + +## Drift + +| Drift | Severity | Logged in drift.md | +| --- | --- | --- | +| Issue title/body still describe manifest emission; authoritative F1(b) re-scopes to CLI hardening. | significant | yes | + +## Gate Results + +### Static Gates + +| Gate | Command or check | Result | Notes | +| --- | --- | --- | --- | +| plan gate | composed milestone ruling | PASS | `plan-eval.md` is explicitly not a local formal eval. | + +### Fitness Gates + +| Gate | Result | Evidence | Notes | +| --- | --- | --- | --- | +| Archetype-2 | NOT_RUN | queued | implementation not started | + +### Runtime Gates + +| Gate | Result | Evidence | Notes | +| --- | --- | --- | --- | +| AppHost serialization | PASS | `aspire ps --format Json` | Baseline owns slot; this run did not start resources. | +| `scaffold.runtime` | NOT_RUN | queued | wait for baseline slot release | + +### Consumer Gates + +| Consumer | Result | Evidence | Notes | +| --- | --- | --- | --- | +| scaffolded app | NOT_RUN | queued | S6 currently not landed; directory fixture fallback selected. | + +## Handoff Notes + +- Evaluator should inspect identity rejection fixtures and the post-describe run-stability check first. + From 637431952e8ece082687dbe11c6bd85b5610219a Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Tue, 4 Aug 2026 09:33:40 +0200 Subject: [PATCH 2/6] feat(mcp): bind Aspire discovery to live AppHost --- .../context-pack.md | 26 ++- .../worklog.md | 16 +- packages/mcp/README.md | 9 +- .../service-endpoints/aspire-cli-command.ts | 36 +++ .../aspire-cli-endpoint-source.ts | 212 +++++++++++++----- .../service-endpoints/aspire-cli-output.ts | 64 ++++++ .../ports/service-endpoint-directory-port.ts | 1 + packages/mcp/src/publish-assets.generated.ts | 2 +- .../tests/service-endpoint-source-fixtures.ts | 2 + .../tests/service-endpoint-sources_test.ts | 76 +++++-- 10 files changed, 357 insertions(+), 87 deletions(-) create mode 100644 packages/mcp/src/infrastructure/service-endpoints/aspire-cli-command.ts create mode 100644 packages/mcp/src/infrastructure/service-endpoints/aspire-cli-output.ts diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md index 8387158ac0..81e2f90874 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md @@ -6,32 +6,32 @@ | --- | --- | | Run ID | `feat-openapi-mcp-manifest-cli--1133` | | Branch | `feat/openapi-mcp-manifest-cli` | -| Current phase | `implement` | +| Current phase | `gate` | | Archetype | `2 - Integration` | | Scope overlays | `none` | ## Current State -Branch is fast-forwarded to main after S5. Research and design are locked for the F1(b) adapter -hardening. Baseline verification owns the AppHost slot; no expensive gate has started here. +The identity-bound F1(b) adapter slice is implemented and its package/JSR gates pass. Baseline +verification still owns the AppHost slot; no expensive gate has started here. ## Completed - Required skills/docs, issue, RFC §F1, P1 verdict, doctrine, gate matrix, and real Aspire output read. - S6 checked: open with no PR, so directory fixture is current E2E fallback. - Composed/not-local PLAN-EVAL ruling recorded. +- Adapter fixtures cover CLI absence, command failure, benign format drift, torn output, foreign + project resources, and AppHost restart races. +- Package check/tests, scoped lint/fmt, quality/architecture, docs, and publish dry-run gates pass. ## In Progress -- Implement pre/post `aspire ps` identity binding, project-root validation, drift-tolerant parsing, - explicit failures, and fixtures. +- Commit/push slice 2 and queue the serialized scaffold runtime proof. ## Next Steps -1. Implement adapter and focused tests. -2. Run Archetype-2/package/JSR gates and review the slice. -3. Re-check S6 and AppHost slot, then run serialized scaffold runtime evidence. -4. Push explicit refspec, maintain draft PR phase trail, and hand to composed evaluation. +1. Re-check S6 and AppHost slot, then run serialized scaffold runtime evidence. +2. Push explicit refspec, maintain draft PR phase trail, and hand to composed evaluation. ## Key Decisions @@ -46,13 +46,17 @@ hardening. Baseline verification owns the AppHost slot; no expensive gate has st | Path | Status | Notes | | --- | --- | --- | | `.llm/runs/feat-openapi-mcp-manifest-cli--1133/*` | new | Harness bootstrap and locked design. | +| `packages/mcp/src/infrastructure/service-endpoints/aspire-cli-*.ts` | changed/new | identity-bound query, spawn boundary, output parser | +| `packages/mcp/src/ports/service-endpoint-directory-port.ts` | changed | finite `run_id_mismatch` failure code | +| `packages/mcp/tests/service-endpoint-*` | changed | negative and drift fixtures | +| `packages/mcp/README.md` + generated asset | changed | production behavior and permissions | ## Gates | Gate family | Current status | Evidence | | --- | --- | --- | -| Static | pending | implementation not started | -| Fitness | pending | implementation not started | +| Static | pass | check + scoped lint/fmt | +| Fitness | pass | focused/package tests, quality scan, arch check, JSR dry run | | Runtime | queued | baseline owns AppHost slot | | Consumer | queued | directory fixture unless S6 lands | diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md index 186a798a12..1e0424b069 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md @@ -55,6 +55,10 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | --- | --- | --- | --- | | 2026-08-04 | 1 | bootstrap | Read brief, five named skills, tooling/JSR guidance, #1133, RFC §F1, P1 verdict, doctrine, matrix, and real CLI output. | | 2026-08-04 | 1 | serialization | `aspire ps` showed baseline-owned AppHosts; runtime gate queued. | +| 2026-08-04 | 2 | implementation | Added pre/post AppHost process binding, real project ownership checks, balanced/case-tolerant JSON parsing, and explicit identity failures. | +| 2026-08-04 | 2 | review | Split spawn and output parsing from the 370-line draft; final files are 36/282/64 lines. | +| 2026-08-04 | 2 | live fault proof | A removed baseline AppHost returned `run_id_mismatch` with zero candidates; a current baseline AppHost returned `used`. | +| 2026-08-04 | 2 | reconcile | S6 still open; no E2E tool-path dependency absorbed. AppHost gate remains serialized. | ## Decisions @@ -77,18 +81,27 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | Gate | Command or check | Result | Notes | | --- | --- | --- | --- | | plan gate | composed milestone ruling | PASS | `plan-eval.md` is explicitly not a local formal eval. | +| package check | `deno task --config packages/mcp/deno.json check` | PASS | all three entrypoints checked | +| scoped lint | run-deno-lint with package config | PASS | 90 files, zero findings | +| scoped fmt | run-deno-fmt with package config | PASS | 90 files, zero findings | ### Fitness Gates | Gate | Result | Evidence | Notes | | --- | --- | --- | --- | -| Archetype-2 | NOT_RUN | queued | implementation not started | +| focused fault/integration tests | PASS | 17/17 | source and directory suites | +| package tests | PASS | 95/95 | `packages/mcp` task | +| quality scan | PASS | repository scanner | zero findings | +| arch check | PASS with baseline warnings | root task | zero failures; unrelated existing warnings only | +| F-5/F-6 JSR | PASS | `deno doc --lint` + `deno publish --dry-run --allow-dirty` | 3 exports checked; no slow types; dry run success | ### Runtime Gates | Gate | Result | Evidence | Notes | | --- | --- | --- | --- | | AppHost serialization | PASS | `aspire ps --format Json` | Baseline owns slot; this run did not start resources. | +| live stale-run refusal | PASS | direct adapter read | removed AppHost produced `run_id_mismatch`, no candidates | +| live current-run query | PASS | direct adapter read | current baseline DB-operation AppHost produced `used` | | `scaffold.runtime` | NOT_RUN | queued | wait for baseline slot release | ### Consumer Gates @@ -100,4 +113,3 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu ## Handoff Notes - Evaluator should inspect identity rejection fixtures and the post-describe run-stability check first. - diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 1c1376c82c..90d0645a97 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -225,7 +225,12 @@ const { entries, sources } = await endpoints.list(); The effective per-service precedence is `override > aspire-cli > run-manifest > appsettings`. Every source remains visible as `used`, `absent`, or `failed`; a failed Aspire CLI query or a stale -manifest is never rendered as healthy absence. The manifest at `.netscript/run/endpoints.json` is +manifest is never rendered as healthy absence. The Aspire adapter brackets its machine-readable +`describe` query with `ps` snapshots: the exact real AppHost path must belong to the project, its +process identity must remain stable across the read, and any executable resource working directory +must remain inside that same real project root. CLI absence, non-zero exit, identity drift, and +partial JSON are distinct failed source rows. Benign CLI banners, trailers, and casing changes are +accepted without weakening those checks. The manifest at `.netscript/run/endpoints.json` is eligible only when its real project root and `runId` match the supplied current run. `appHostPath` defaults to `./aspire/apphost.mts`; override it when the active AppHost lives elsewhere. Supply `expectedRunId` only when the host owns the current AppHost run token; without that identity proof, @@ -253,7 +258,7 @@ JSON containing its selected service name, for example `{ "service": "orders" }` base path; this second request prevents a reused port from being mistaken for the intended service. The default library composition needs `--allow-read` for carriers and real-path checks, -`--allow-run` for `aspire describe`, and `--allow-net` for bounded spec/identity requests. Tests and +`--allow-run` for `aspire ps` plus `aspire describe`, and `--allow-net` for bounded spec/identity requests. Tests and custom hosts can replace every source and the probe through `ServiceEndpointDirectoryOptions`. ## Configuration at a glance diff --git a/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-command.ts b/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-command.ts new file mode 100644 index 0000000000..04f7a20759 --- /dev/null +++ b/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-command.ts @@ -0,0 +1,36 @@ +/** Execute one cancellable Aspire CLI query with fully captured output. */ +export async function executeAspireCliCommand( + command: string, + args: readonly string[], + signal?: AbortSignal, +): Promise<{ readonly code: number; readonly stdout: string; readonly stderr: string }> { + signal?.throwIfAborted(); + const child = new Deno.Command(command, { + args: [...args], + stdout: 'piped', + stderr: 'piped', + }).spawn(); + let aborted = false; + const abort = (): void => { + aborted = true; + try { + child.kill('SIGTERM'); + } catch (error) { + if (!(error instanceof Deno.errors.NotFound)) throw error; + } + }; + signal?.addEventListener('abort', abort, { once: true }); + let output: Deno.CommandOutput; + try { + output = await child.output(); + } finally { + signal?.removeEventListener('abort', abort); + } + if (aborted) throw new DOMException('Aspire CLI query was aborted', 'AbortError'); + const decoder = new TextDecoder(); + return { + code: output.code, + stdout: decoder.decode(output.stdout), + stderr: decoder.decode(output.stderr), + }; +} diff --git a/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-endpoint-source.ts b/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-endpoint-source.ts index 7d3a5e5d68..230a6716a9 100644 --- a/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-endpoint-source.ts +++ b/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-endpoint-source.ts @@ -1,11 +1,20 @@ -import { join } from '@std/path'; +import { isAbsolute, join, relative } from '@std/path'; import type { EndpointCandidate, EndpointSourceContext, EndpointSourcePort, + FailedSourceOutcome, SourceFailureCode, SourceOutcome, } from '../../ports/service-endpoint-directory-port.ts'; +import { executeAspireCliCommand } from './aspire-cli-command.ts'; +import { + aspireField, + aspireIdentityField, + aspireStringField, + extractAspireJson, + isAspireRecord, +} from './aspire-cli-output.ts'; import { normalizeDiscoveredEndpointUrl } from './endpoint-url.ts'; /** Captured result of one Aspire CLI invocation. */ @@ -31,23 +40,55 @@ export interface AspireCliEndpointSourceOptions { readonly command?: string; /** Override the process boundary for deterministic tests. */ readonly execute?: AspireCliCommand; + /** Override real-path resolution for deterministic identity tests. */ + readonly realPath?: (path: string) => Promise; +} + +interface AspireAppHostRun { + readonly appHostPath: string; + readonly runId: string; } /** Queries the running AppHost through Aspire's machine-readable describe surface. */ export class AspireCliEndpointSource implements EndpointSourcePort { readonly #command: string; readonly #execute: AspireCliCommand; + readonly #realPath: (path: string) => Promise; /** Create an Aspire source with an injectable command boundary. */ constructor(options: AspireCliEndpointSourceOptions = {}) { this.#command = options.command ?? 'aspire'; - this.#execute = options.execute ?? executeCommand; + this.#execute = options.execute ?? executeAspireCliCommand; + this.#realPath = options.realPath ?? Deno.realPath; } /** Report command absence, non-zero exits, and parse failures as distinct failed outcomes. */ async read(context: EndpointSourceContext, signal?: AbortSignal): Promise { signal?.throwIfAborted(); const appHostPath = context.appHostPath ?? join(context.projectRoot, 'aspire', 'apphost.mts'); + let projectIdentity: { readonly root: string; readonly appHost: string }; + try { + projectIdentity = { + root: await this.#realPath(context.projectRoot), + appHost: await this.#realPath(appHostPath), + }; + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') throw error; + return failed( + 'project_root_mismatch', + `Aspire project identity could not be resolved: ${describe(error)}`, + ); + } + if (!isWithin(projectIdentity.root, projectIdentity.appHost)) { + return failed( + 'project_root_mismatch', + `AppHost path is outside the requested project root: ${projectIdentity.appHost}`, + ); + } + + const before = await this.#readRun(projectIdentity.appHost, signal); + if (before.outcome === 'failed') return before; + let result: AspireCliCommandResult; try { result = await this.#execute(this.#command, [ @@ -76,76 +117,125 @@ export class AspireCliEndpointSource implements EndpointSourcePort { let parsed: unknown; try { - parsed = JSON.parse(extractJson(result.stdout)); + parsed = JSON.parse(extractAspireJson(result.stdout)); } catch (error) { return failed('parse_failed', `aspire describe JSON could not be parsed: ${describe(error)}`); } - if (!isRecord(parsed) || !Array.isArray(parsed['resources'])) { - return failed('parse_failed', 'aspire describe output did not contain top-level resources[]'); + const resources = aspireField(parsed, 'resources'); + if (!Array.isArray(resources)) { + return failed( + 'parse_failed', + 'aspire describe output did not contain top-level resources[]', + ); } const candidates: EndpointCandidate[] = []; - for (const rawResource of parsed['resources']) { - if (!isRecord(rawResource)) continue; + for (const rawResource of resources) { + if (!isAspireRecord(rawResource)) continue; const name = resourceName(rawResource); - const baseUrl = firstHttpUrl(rawResource['urls']); + const baseUrl = firstHttpUrl(aspireField(rawResource, 'urls', 'endpoints')); if (!name || !baseUrl) continue; + const workDir = resourceWorkDir(rawResource); + if (workDir) { + let realWorkDir: string; + try { + realWorkDir = await this.#realPath(workDir); + } catch (error) { + return failed( + 'project_root_mismatch', + `Resource ${name} working directory could not be resolved: ${describe(error)}`, + ); + } + if (!isWithin(projectIdentity.root, realWorkDir)) { + return failed( + 'project_root_mismatch', + `Resource ${name} belongs to a foreign project root: ${realWorkDir}`, + ); + } + } candidates.push({ name, baseUrl, source: 'aspire-cli', operatorTrusted: false }); } + + const after = await this.#readRun(projectIdentity.appHost, signal); + if (after.outcome === 'failed') return after; + if (after.run.runId !== before.run.runId) { + return failed( + 'run_id_mismatch', + `AppHost restarted while aspire describe was read (${before.run.runId} -> ${after.run.runId})`, + ); + } candidates.sort((left, right) => left.name.localeCompare(right.name)); return { source: 'aspire-cli', outcome: 'used', candidates, excludedServices: [] }; } -} -async function executeCommand( - command: string, - args: readonly string[], - signal?: AbortSignal, -): Promise { - signal?.throwIfAborted(); - const child = new Deno.Command(command, { - args: [...args], - stdout: 'piped', - stderr: 'piped', - }).spawn(); - let aborted = false; - const abort = (): void => { - aborted = true; + async #readRun( + appHostPath: string, + signal?: AbortSignal, + ): Promise< + { readonly outcome: 'current'; readonly run: AspireAppHostRun } | FailedSourceOutcome + > { + let result: AspireCliCommandResult; + try { + result = await this.#execute(this.#command, [ + 'ps', + '--format', + 'Json', + '--non-interactive', + '--nologo', + ], signal); + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + return failed('command_not_found', `Aspire CLI executable was not found: ${this.#command}`); + } + if (error instanceof DOMException && error.name === 'AbortError') throw error; + return failed('command_failed', describe(error)); + } + if (result.code !== 0) { + return failed('command_failed', commandFailure('aspire ps', result)); + } + + let parsed: unknown; try { - child.kill('SIGTERM'); + parsed = JSON.parse(extractAspireJson(result.stdout)); } catch (error) { - if (!(error instanceof Deno.errors.NotFound)) throw error; + return failed('parse_failed', `aspire ps JSON could not be parsed: ${describe(error)}`); + } + const rawRuns = Array.isArray(parsed) ? parsed : aspireField(parsed, 'appHosts', 'items'); + if (!Array.isArray(rawRuns)) { + return failed('parse_failed', 'aspire ps output did not contain an AppHost array'); } - }; - signal?.addEventListener('abort', abort, { once: true }); - let output: Deno.CommandOutput; - try { - output = await child.output(); - } finally { - signal?.removeEventListener('abort', abort); - } - if (aborted) throw new DOMException('Aspire CLI query was aborted', 'AbortError'); - const decoder = new TextDecoder(); - return { - code: output.code, - stdout: decoder.decode(output.stdout), - stderr: decoder.decode(output.stderr), - }; -} -function extractJson(text: string): string { - const trimmed = text.trim(); - if (trimmed.startsWith('{') || trimmed.startsWith('[')) return trimmed; - const positions = [trimmed.indexOf('{'), trimmed.indexOf('[')].filter((index) => index >= 0); - if (positions.length === 0) throw new SyntaxError('no JSON object or array was emitted'); - return trimmed.slice(Math.min(...positions)); + const runs: AspireAppHostRun[] = []; + for (const rawRun of rawRuns) { + if (!isAspireRecord(rawRun)) continue; + const rawPath = aspireStringField(rawRun, 'appHostPath'); + const rawPid = aspireIdentityField(rawRun, 'appHostPid'); + if (!rawPath || rawPid === undefined) continue; + let realRunPath: string; + try { + realRunPath = await this.#realPath(rawPath); + } catch { + continue; + } + if (realRunPath === appHostPath) { + runs.push({ appHostPath: realRunPath, runId: String(rawPid) }); + } + } + if (runs.length !== 1) { + return failed( + 'run_id_mismatch', + `aspire ps identified ${runs.length} current runs for AppHost ${appHostPath}`, + ); + } + return { outcome: 'current', run: runs[0]! }; + } } function resourceName(resource: Record): string | undefined { - const displayName = resource['displayName']; - if (typeof displayName === 'string' && displayName) return displayName; - const name = resource['name']; - if (typeof name !== 'string' || !name) return undefined; + const displayName = aspireStringField(resource, 'displayName'); + if (displayName) return displayName; + const name = aspireStringField(resource, 'name'); + if (!name) return undefined; return name.replace(/-[a-z0-9]{8}$/i, ''); } @@ -153,18 +243,30 @@ function firstHttpUrl(value: unknown): string | undefined { if (!Array.isArray(value)) return undefined; const urls: string[] = []; for (const entry of value) { - const rawUrl = isRecord(entry) ? entry['url'] : entry; + const rawUrl = isAspireRecord(entry) ? aspireField(entry, 'url') : entry; const normalized = normalizeDiscoveredEndpointUrl(rawUrl, false); if (normalized) urls.push(normalized); } return urls.find((url) => url.startsWith('http://')) ?? urls[0]; } -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); +function resourceWorkDir(resource: Record): string | undefined { + const properties = aspireField(resource, 'properties'); + if (!isAspireRecord(properties)) return undefined; + return aspireStringField(properties, 'executable.workDir', 'workDir'); +} + +function isWithin(root: string, target: string): boolean { + const path = relative(root, target); + return path === '' || (!path.startsWith('..') && !isAbsolute(path)); +} + +function commandFailure(command: string, result: AspireCliCommandResult): string { + const detail = (result.stderr || result.stdout).trim().slice(0, 500); + return `${command} exited ${result.code}${detail ? `: ${detail}` : ''}`; } -function failed(code: SourceFailureCode, reason: string): SourceOutcome { +function failed(code: SourceFailureCode, reason: string): FailedSourceOutcome { return { source: 'aspire-cli', outcome: 'failed', diff --git a/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-output.ts b/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-output.ts new file mode 100644 index 0000000000..0f2ec83005 --- /dev/null +++ b/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-output.ts @@ -0,0 +1,64 @@ +/** Extract one complete JSON value from Aspire banner or trailer noise. */ +export function extractAspireJson(text: string): string { + const trimmed = text.trim(); + const positions = [trimmed.indexOf('{'), trimmed.indexOf('[')].filter((index) => index >= 0); + if (positions.length === 0) throw new SyntaxError('no JSON object or array was emitted'); + const start = Math.min(...positions); + const stack: string[] = []; + let inString = false; + let escaped = false; + for (let index = start; index < trimmed.length; index++) { + const character = trimmed[index]; + if (inString) { + if (escaped) escaped = false; + else if (character === '\\') escaped = true; + else if (character === '"') inString = false; + continue; + } + if (character === '"') { + inString = true; + continue; + } + if (character === '{' || character === '[') stack.push(character); + if (character === '}' || character === ']') { + const expected = character === '}' ? '{' : '['; + if (stack.pop() !== expected) throw new SyntaxError('JSON delimiters were mismatched'); + if (stack.length === 0) return trimmed.slice(start, index + 1); + } + } + throw new SyntaxError('JSON output was incomplete'); +} + +/** Read a field while tolerating casing drift in machine-readable Aspire output. */ +export function aspireField(record: unknown, ...names: readonly string[]): unknown { + if (!isAspireRecord(record)) return undefined; + const keys = Object.keys(record); + for (const name of names) { + const key = keys.find((candidate) => candidate.toLowerCase() === name.toLowerCase()); + if (key !== undefined) return record[key]; + } + return undefined; +} + +/** Read a non-empty string field from machine-readable Aspire output. */ +export function aspireStringField( + record: Record, + ...names: readonly string[] +): string | undefined { + const value = aspireField(record, ...names); + return typeof value === 'string' && value.length > 0 ? value : undefined; +} + +/** Read a process identity field without assuming whether the CLI encodes it as JSON text or number. */ +export function aspireIdentityField( + record: Record, + ...names: readonly string[] +): number | string | undefined { + const value = aspireField(record, ...names); + return typeof value === 'number' || typeof value === 'string' ? value : undefined; +} + +/** Test whether an unknown Aspire JSON value is an object record. */ +export function isAspireRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/mcp/src/ports/service-endpoint-directory-port.ts b/packages/mcp/src/ports/service-endpoint-directory-port.ts index d8681fd887..6b5e6b65d7 100644 --- a/packages/mcp/src/ports/service-endpoint-directory-port.ts +++ b/packages/mcp/src/ports/service-endpoint-directory-port.ts @@ -27,6 +27,7 @@ export const SOURCE_FAILURE_CODES = [ 'command_not_found', 'command_failed', 'parse_failed', + 'run_id_mismatch', 'source_failed', ] as const; diff --git a/packages/mcp/src/publish-assets.generated.ts b/packages/mcp/src/publish-assets.generated.ts index 143ba02474..eec7100992 100644 --- a/packages/mcp/src/publish-assets.generated.ts +++ b/packages/mcp/src/publish-assets.generated.ts @@ -6,4 +6,4 @@ export const MCP_PACKAGE_VERSION: string = '0.0.4'; /** Published MCP README embedded as the default documentation corpus. */ export const MCP_PACKAGE_README: string = - '# @netscript/mcp\n\n[![JSR](https://jsr.io/badges/@netscript/mcp)](https://jsr.io/@netscript/mcp)\n[![CI](https://github.com/rickylabs/netscript/actions/workflows/ci.yml/badge.svg)](https://github.com/rickylabs/netscript/actions/workflows/ci.yml)\n[![Docs](https://img.shields.io/badge/docs-rickylabs.github.io-blue)](https://rickylabs.github.io/netscript/)\n\n**The Model Context Protocol server for NetScript: 17 token-bounded tools that let a coding agent\nmonitor a running app, debug a correlated execution, read framework-semantic telemetry, run the\ndoctor, and search the docs — all over stdio.**\n\nPoint Claude Code or VS Code at a running NetScript app and the agent can ask _"is the app\nhealthy?"_, _"why did the last import job fail?"_, and _"what is slowing down `checkout`?"_ — and\nget compact, structured answers instead of raw logs. It can correlate one execution\'s spans, logs,\nand outcome by id; rank the queries hammering your database; and trigger allowlisted CLI commands\nthrough a default-deny policy. One command — `netscript agent init` — wires all of it into your\nagent host.\n\nGeneric observability tooling hands an agent raw spans and log lines and lets it burn its context\nwindow re-deriving structure the framework already knows. `@netscript/mcp` answers in NetScript\'s\nown vocabulary — jobs, sagas, triggers, streams, services — and bounds every result server-side, so\nthe agent gets percentiles, error rates, and ranked operations rather than the spans they were\ncomputed from. It reads the same OpenTelemetry data the Aspire dashboard shows you, and complements\nAspire\'s own MCP server: Aspire speaks resources and containers; this server speaks your app.\n\n## Why agents like it\n\n- **17 token-bounded tools** — every successful result is capped server-side (50 array items, 2,000\n characters per string) before it reaches the model; the analytics tools never return raw spans at\n all.\n- **Framework-semantic trace intelligence** — tools classify telemetry into `worker`, `saga`,\n `trigger`, `stream`, and `service` domains and correlate whole executions by id, because they\n understand the `netscript.*` attribute conventions the framework emits.\n- **Default-deny CLI gate** — `execute_command` matches commands against an ordered prefix policy;\n deny beats allow, anything unmatched is denied, and the shipped policy explicitly denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove`.\n- **One-command install** — `netscript agent init` detects your agent host, writes the MCP\n configuration, and installs the matching NetScript skills.\n- **Matched agent surface** — `netscript agent init` writes host configuration pinned to your\n installed CLI version and installs the skills that ship with that same release, so the tool\n catalog the agent sees comes from the release it runs.\n- **Zero npm MCP SDK** — a minimal newline-delimited JSON-RPC transport keeps the dependency graph\n lean and the lockfile stable.\n\n## Architecture\n\n```mermaid\nflowchart LR\n A["Agent host
(Claude Code, VS Code, ...)"] <-- "JSON-RPC / stdio" --> S["netscript agent mcp
17 tools · bounded results"]\n S --> T["Telemetry endpoint
(OTLP read model)"]\n S --> D["Docs corpus
(public Markdown)"]\n S --> P["Command policy
(default-deny allowlist)"]\n T --> R["Running NetScript app"]\n P --> C["netscript CLI"]\n C --> R\n```\n\nThe server is one third of the NetScript agent surface — the CLI is the hands, the skills are the\nplaybook, MCP is the eyes. It deliberately wraps the CLI rather than reimplementing it:\n`list_commands` reflects the live command tree, and `execute_command` shells the CLI through the\npolicy gate. MCP exists for what a shell cannot cheaply give an agent — bounded aggregation,\ncross-domain diagnostics, and documentation lookup.\n\n## Install\n\nMost users never import this package. Install the server into a project with the CLI:\n\n```bash\nnetscript agent init\n```\n\nThat detects your agent host and writes `.mcp.json` (Claude Code) and/or `.vscode/mcp.json` (VS\nCode) pointing at `netscript agent mcp`, and installs the NetScript skills shipped with your CLI\nrelease. Use `--host claude|vscode|all` to choose explicitly.\n\nTo embed the server in your own host process, add it as a library:\n\n```bash\ndeno add jsr:@netscript/mcp@\n```\n\nTo run the standalone stdio entrypoint directly when integrating another MCP host:\n\n```bash\ndeno x -A jsr:@netscript/mcp@/cli\n```\n\nPin `` to match your installed CLI; bare `jsr:@netscript/*` specifiers do not resolve on\nthe pre-release line, and `netscript agent init` writes the correct pinned form for you.\n\n## Quick example\n\n**1. Wire up an agent host.** From a NetScript project root:\n\n```bash\n$ netscript agent init\nInstalled NetScript agent integration for claude, vscode.\n```\n\nThe generated `.mcp.json` runs the server for this project — equivalent to:\n\n```json\n{\n "mcpServers": {\n "netscript": {\n "command": "deno",\n "args": [\n "run",\n "-A",\n "jsr:@netscript/cli@",\n "agent",\n "mcp",\n "--project-root",\n ""\n ]\n }\n }\n}\n```\n\n**2. Ask the agent.** With the app started, the agent turns questions into bounded tool calls:\n\n> **You:** Is the app healthy? Anything in the docs about telemetry?\n>\n> **Agent:** calls `get_app_status` →\n> `{"status": "…", "counts": {…}, "domains": [{"domain": "worker", …}, …]}` — a health verdict with\n> per-domain summaries, not a span dump. Calls `search_docs {"query": "telemetry"}` →\n> `{"count": 1, "matches": [{"slug": "mcp", "title": "@netscript/mcp", "snippet": "…", "score": 35}]}`,\n> then `get_doc` with the winning slug to read just the section it needs.\n\nWhen telemetry is unreachable, nothing crashes: `get_app_status` and the doctor\'s telemetry checks\nreport a structured `warn`/`fail` status, the list and analytics tools return their ordinary empty\nresults, and `get_run` returns a structured `run_not_found` error the agent can reason about.\n\n## Tool catalog\n\n| Tool | Required input | Bounded result |\n| ----------------------------- | ----------------- | ---------------------------------------------------------------------------- |\n| `get_app_status` | — | Health verdict, counts, per-domain summaries |\n| `list_runs` | — | Recent executions filtered by domain, status, service, time |\n| `get_run` | `id` | One correlated execution with bounded spans and logs |\n| `get_recent_errors` | — | Recent errors grouped by service and domain |\n| `get_last_job_result` | — | The latest matching job outcome |\n| `analyze_service_performance` | `service` | Duration percentiles, throughput, error rate |\n| `analyze_db_bottlenecks` | — | Ranked database and KV operations |\n| `doctor` | — | Telemetry, Aspire, wiring, and plugin checks; suggested fixes on problems |\n| `search_docs` | `query` | Ranked public-document matches with snippets |\n| `list_docs` | — | Public-document summaries |\n| `get_doc` | `slug` | One public document, or one named section of it |\n| `list_commands` | — | Live CLI command descriptors |\n| `execute_command` | `command` | Exit code, duration, and bounded output tail; structured denial when blocked |\n| `record_drift` | `resource`, `summary` | Evidence-gated drift entry appended to project drift log |\n| `list_api_services` | — | Discovered services, live spec status, source outcomes, and operation counts |\n| `list_service_operations` | `service` | Bounded OpenAPI operation rows with honest truncation metadata |\n| `get_operation_schema` | `service`, `operation` | Request, response, and error views plus an unauthenticated curl template |\n\nA top-level input/result field overview for every tool is on the\n[MCP reference](https://rickylabs.github.io/netscript/reference/mcp/); the complete Standard Schema\ncontracts are published as `TOOL_INPUT_SCHEMAS` / `TOOL_OUTPUT_SCHEMAS` and returned by the live\n`tools/list`.\n\n## Record drift\n\n`record_drift` is an evidence-gated mutating tool that records verified architecture or runtime drift into `.netscript/agent/drift.jsonl`.\n\n- **Required evidence**: Requires a fresh successful diagnostic receipt (timestamped within 15 minutes, `exitStatus: 0`) for the target resource. Receipts are automatically produced when calling `doctor`, telemetry tools, or `netscript plugin doctor --resource `.\n- **Target & Scope**: The `resource` argument targets a specific plugin, service, or `\'project\'`. Receipts live at `.netscript/agent/diagnostics/.json`.\n- **Mutation behavior**: Appends a single JSON line to `.netscript/agent/drift.jsonl` under the project root containing `timestamp`, `resource`, `summary`, optional `details`, and the attached evidence receipt.\n- **Failure modes**: If no receipt exists, if the receipt is older than 15 minutes, or if the receipt recorded a non-zero exit status, `record_drift` refuses with structured error code `diagnostic_evidence_required`.\n- **Dry-run / Preview**: Inspecting receipts or running `doctor` / telemetry tools previews current diagnostic state without mutating `drift.jsonl`.\n\n## Embedding as a library\n\nTo run the public stdio composition from your own Deno entrypoint:\n\n```ts\nimport { runMcpStdioServer } from \'@netscript/mcp/cli\';\n\nawait runMcpStdioServer({\n projectRoot: Deno.cwd(),\n // Omit docsRoot to use the package-embedded corpus, or select a filesystem corpus explicitly.\n docsRoot: Deno.env.get(\'NETSCRIPT_DOCS_ROOT\'),\n});\n```\n\n`runMcpStdioServer` owns the newline-delimited stdio transport. It shuts down when the host closes\nstdin or terminates the process; callers do not need to reach into an internal transport API.\n\n## Public surface\n\nThree entrypoints carry the package:\n\n| Entry | What it gives you |\n| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |\n| `.` | Tool contracts and schemas, the tool registry, protocol runner, service endpoint directory ports, and default adapters |\n| `./cli` | The executable composition plus every export from `.`, including the service endpoint directory surface |\n| `./openapi-projection` | Pure OpenAPI operation indexing and schema projections, with no discovery, filesystem, network, or runtime work |\n\nThe projection subpath accepts an already-loaded OpenAPI document. It keeps discovery and I/O at\nthe caller\'s boundary:\n\n```ts\nimport { indexOpenApiOperations } from \'@netscript/mcp/openapi-projection\';\n\nconst index = indexOpenApiOperations(openApiDocument);\n\nEvery tool flow depends on a port interface, so embedders and tests supply their own adapters and\nassert against the published schemas. The always-current symbol list is\n[`deno doc jsr:@netscript/mcp@`](https://jsr.io/@netscript/mcp/doc) (pin `` on the\npre-release line, as above).\n\n### Discover service OpenAPI endpoints\n\nEmbedders can compose the four discovery sources and bounded network probe without importing an\nOpenAPI projection layer:\n\n```ts\nimport { createServiceEndpointDirectory } from \'@netscript/mcp\';\n\nconst endpoints = createServiceEndpointDirectory({\n projectRoot: Deno.cwd(),\n});\n\nconst { entries, sources } = await endpoints.list();\n```\n\nThe effective per-service precedence is `override > aspire-cli > run-manifest > appsettings`. Every\nsource remains visible as `used`, `absent`, or `failed`; a failed Aspire CLI query or a stale\nmanifest is never rendered as healthy absence. The manifest at `.netscript/run/endpoints.json` is\neligible only when its real project root and `runId` match the supplied current run. `appHostPath`\ndefaults to `./aspire/apphost.mts`; override it when the active AppHost lives elsewhere. Supply\n`expectedRunId` only when the host owns the current AppHost run token; without that identity proof,\na present run manifest is reported as failed and does not contribute endpoints.\n\nExplicit operator endpoints and exclusions live only in the S5-owned subsection of\n`.netscript/agent-mcp.json`; sibling settings are ignored:\n\n```json\n{\n "introspection": {\n "serviceEndpoints": {\n "orders": "https://orders.example.test"\n },\n "excludeServices": ["internal-admin"]\n }\n}\n```\n\nExclusions are applied before network access. Other rows report `running`, `not_running`,\n`spec_unavailable`, or `identity_mismatch`; parsed OpenAPI is retained as opaque JSON for a later\nconsumer. Probes do not send credentials or follow redirects. A 401/403 explains how to expose only\nthe OpenAPI route anonymously or supply a reachable public spec URL. A running service must return\nJSON containing its selected service name, for example `{ "service": "orders" }`, from its selected\nbase path; this second request prevents a reused port from being mistaken for the intended service.\n\nThe default library composition needs `--allow-read` for carriers and real-path checks,\n`--allow-run` for `aspire describe`, and `--allow-net` for bounded spec/identity requests. Tests and\ncustom hosts can replace every source and the probe through `ServiceEndpointDirectoryOptions`.\n\n## Configuration at a glance\n\n- **Telemetry endpoint discovery** (tools and `doctor`): explicit `--endpoint`, then\n `NETSCRIPT_TELEMETRY_ENDPOINT`, then `ASPIRE_DASHBOARD_PORT`, then `http://localhost:18888`.\n- **Docs corpus**: by default the docs tools index the documentation shipped with the installed\n package; set `--docs-root ` (or `NETSCRIPT_DOCS_ROOT`) to serve a project or site corpus\n instead.\n- **Service endpoint discovery** (library surface): `.netscript/agent-mcp.json` override, then the\n Aspire CLI machine-readable query, then an identity-bound run manifest, then\n `aspire/appsettings.json`; lower-priority disagreements remain visible as conflicts.\n- **Command policy**: the shipped default allows the prefixes\n `db init|generate|migrate|seed|status|introspect`, `generate`, `contract`, `service list`,\n `plugin install|list|sync|doctor`, and `ui:add|ui:init|ui:list|ui:update`, and denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove` — deny beats allow, anything\n unmatched is denied. Embedders can pass their own policy.\n\nThe full flag reference, policy table, and composition options are on the docs site.\n\n## Docs\n\n- **MCP reference — the 17-tool field overview, policy, and exports**:\n [rickylabs.github.io/netscript/reference/mcp/](https://rickylabs.github.io/netscript/reference/mcp/)\n- **Agent tooling — install, flags, troubleshooting, CLI × skills × MCP**:\n [rickylabs.github.io/netscript/capabilities/agent-tooling/](https://rickylabs.github.io/netscript/capabilities/agent-tooling/)\n- **API docs on JSR**: [jsr.io/@netscript/mcp/doc](https://jsr.io/@netscript/mcp/doc)\n\n## Compatibility\n\nThe **server** requires Deno 2.9+ (both entrypoints use `Deno.*` APIs); Node.js and Bun are not\nsupported as server runtimes. The **client** side is unconstrained: any MCP-capable host — Claude\nCode, VS Code, and others — only has to spawn the process and speak JSON-RPC over stdio. The\nexecutable needs `--allow-env`, `--allow-net`, `--allow-read`, and `--allow-run`; the `netscript`\nbinary grants these at its edge. The server never returns project source, environment-variable\nvalues, credentials, or secrets.\n\n## License\n\nApache-2.0 — see [LICENSE](https://github.com/rickylabs/netscript/blob/main/LICENSE). Published to\nJSR with cryptographically verified provenance.\n'; + '# @netscript/mcp\n\n[![JSR](https://jsr.io/badges/@netscript/mcp)](https://jsr.io/@netscript/mcp)\n[![CI](https://github.com/rickylabs/netscript/actions/workflows/ci.yml/badge.svg)](https://github.com/rickylabs/netscript/actions/workflows/ci.yml)\n[![Docs](https://img.shields.io/badge/docs-rickylabs.github.io-blue)](https://rickylabs.github.io/netscript/)\n\n**The Model Context Protocol server for NetScript: 17 token-bounded tools that let a coding agent\nmonitor a running app, debug a correlated execution, read framework-semantic telemetry, run the\ndoctor, and search the docs — all over stdio.**\n\nPoint Claude Code or VS Code at a running NetScript app and the agent can ask _"is the app\nhealthy?"_, _"why did the last import job fail?"_, and _"what is slowing down `checkout`?"_ — and\nget compact, structured answers instead of raw logs. It can correlate one execution\'s spans, logs,\nand outcome by id; rank the queries hammering your database; and trigger allowlisted CLI commands\nthrough a default-deny policy. One command — `netscript agent init` — wires all of it into your\nagent host.\n\nGeneric observability tooling hands an agent raw spans and log lines and lets it burn its context\nwindow re-deriving structure the framework already knows. `@netscript/mcp` answers in NetScript\'s\nown vocabulary — jobs, sagas, triggers, streams, services — and bounds every result server-side, so\nthe agent gets percentiles, error rates, and ranked operations rather than the spans they were\ncomputed from. It reads the same OpenTelemetry data the Aspire dashboard shows you, and complements\nAspire\'s own MCP server: Aspire speaks resources and containers; this server speaks your app.\n\n## Why agents like it\n\n- **17 token-bounded tools** — every successful result is capped server-side (50 array items, 2,000\n characters per string) before it reaches the model; the analytics tools never return raw spans at\n all.\n- **Framework-semantic trace intelligence** — tools classify telemetry into `worker`, `saga`,\n `trigger`, `stream`, and `service` domains and correlate whole executions by id, because they\n understand the `netscript.*` attribute conventions the framework emits.\n- **Default-deny CLI gate** — `execute_command` matches commands against an ordered prefix policy;\n deny beats allow, anything unmatched is denied, and the shipped policy explicitly denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove`.\n- **One-command install** — `netscript agent init` detects your agent host, writes the MCP\n configuration, and installs the matching NetScript skills.\n- **Matched agent surface** — `netscript agent init` writes host configuration pinned to your\n installed CLI version and installs the skills that ship with that same release, so the tool\n catalog the agent sees comes from the release it runs.\n- **Zero npm MCP SDK** — a minimal newline-delimited JSON-RPC transport keeps the dependency graph\n lean and the lockfile stable.\n\n## Architecture\n\n```mermaid\nflowchart LR\n A["Agent host
(Claude Code, VS Code, ...)"] <-- "JSON-RPC / stdio" --> S["netscript agent mcp
17 tools · bounded results"]\n S --> T["Telemetry endpoint
(OTLP read model)"]\n S --> D["Docs corpus
(public Markdown)"]\n S --> P["Command policy
(default-deny allowlist)"]\n T --> R["Running NetScript app"]\n P --> C["netscript CLI"]\n C --> R\n```\n\nThe server is one third of the NetScript agent surface — the CLI is the hands, the skills are the\nplaybook, MCP is the eyes. It deliberately wraps the CLI rather than reimplementing it:\n`list_commands` reflects the live command tree, and `execute_command` shells the CLI through the\npolicy gate. MCP exists for what a shell cannot cheaply give an agent — bounded aggregation,\ncross-domain diagnostics, and documentation lookup.\n\n## Install\n\nMost users never import this package. Install the server into a project with the CLI:\n\n```bash\nnetscript agent init\n```\n\nThat detects your agent host and writes `.mcp.json` (Claude Code) and/or `.vscode/mcp.json` (VS\nCode) pointing at `netscript agent mcp`, and installs the NetScript skills shipped with your CLI\nrelease. Use `--host claude|vscode|all` to choose explicitly.\n\nTo embed the server in your own host process, add it as a library:\n\n```bash\ndeno add jsr:@netscript/mcp@\n```\n\nTo run the standalone stdio entrypoint directly when integrating another MCP host:\n\n```bash\ndeno x -A jsr:@netscript/mcp@/cli\n```\n\nPin `` to match your installed CLI; bare `jsr:@netscript/*` specifiers do not resolve on\nthe pre-release line, and `netscript agent init` writes the correct pinned form for you.\n\n## Quick example\n\n**1. Wire up an agent host.** From a NetScript project root:\n\n```bash\n$ netscript agent init\nInstalled NetScript agent integration for claude, vscode.\n```\n\nThe generated `.mcp.json` runs the server for this project — equivalent to:\n\n```json\n{\n "mcpServers": {\n "netscript": {\n "command": "deno",\n "args": [\n "run",\n "-A",\n "jsr:@netscript/cli@",\n "agent",\n "mcp",\n "--project-root",\n ""\n ]\n }\n }\n}\n```\n\n**2. Ask the agent.** With the app started, the agent turns questions into bounded tool calls:\n\n> **You:** Is the app healthy? Anything in the docs about telemetry?\n>\n> **Agent:** calls `get_app_status` →\n> `{"status": "…", "counts": {…}, "domains": [{"domain": "worker", …}, …]}` — a health verdict with\n> per-domain summaries, not a span dump. Calls `search_docs {"query": "telemetry"}` →\n> `{"count": 1, "matches": [{"slug": "mcp", "title": "@netscript/mcp", "snippet": "…", "score": 35}]}`,\n> then `get_doc` with the winning slug to read just the section it needs.\n\nWhen telemetry is unreachable, nothing crashes: `get_app_status` and the doctor\'s telemetry checks\nreport a structured `warn`/`fail` status, the list and analytics tools return their ordinary empty\nresults, and `get_run` returns a structured `run_not_found` error the agent can reason about.\n\n## Tool catalog\n\n| Tool | Required input | Bounded result |\n| ----------------------------- | ----------------- | ---------------------------------------------------------------------------- |\n| `get_app_status` | — | Health verdict, counts, per-domain summaries |\n| `list_runs` | — | Recent executions filtered by domain, status, service, time |\n| `get_run` | `id` | One correlated execution with bounded spans and logs |\n| `get_recent_errors` | — | Recent errors grouped by service and domain |\n| `get_last_job_result` | — | The latest matching job outcome |\n| `analyze_service_performance` | `service` | Duration percentiles, throughput, error rate |\n| `analyze_db_bottlenecks` | — | Ranked database and KV operations |\n| `doctor` | — | Telemetry, Aspire, wiring, and plugin checks; suggested fixes on problems |\n| `search_docs` | `query` | Ranked public-document matches with snippets |\n| `list_docs` | — | Public-document summaries |\n| `get_doc` | `slug` | One public document, or one named section of it |\n| `list_commands` | — | Live CLI command descriptors |\n| `execute_command` | `command` | Exit code, duration, and bounded output tail; structured denial when blocked |\n| `record_drift` | `resource`, `summary` | Evidence-gated drift entry appended to project drift log |\n| `list_api_services` | — | Discovered services, live spec status, source outcomes, and operation counts |\n| `list_service_operations` | `service` | Bounded OpenAPI operation rows with honest truncation metadata |\n| `get_operation_schema` | `service`, `operation` | Request, response, and error views plus an unauthenticated curl template |\n\nA top-level input/result field overview for every tool is on the\n[MCP reference](https://rickylabs.github.io/netscript/reference/mcp/); the complete Standard Schema\ncontracts are published as `TOOL_INPUT_SCHEMAS` / `TOOL_OUTPUT_SCHEMAS` and returned by the live\n`tools/list`.\n\n## Record drift\n\n`record_drift` is an evidence-gated mutating tool that records verified architecture or runtime drift into `.netscript/agent/drift.jsonl`.\n\n- **Required evidence**: Requires a fresh successful diagnostic receipt (timestamped within 15 minutes, `exitStatus: 0`) for the target resource. Receipts are automatically produced when calling `doctor`, telemetry tools, or `netscript plugin doctor --resource `.\n- **Target & Scope**: The `resource` argument targets a specific plugin, service, or `\'project\'`. Receipts live at `.netscript/agent/diagnostics/.json`.\n- **Mutation behavior**: Appends a single JSON line to `.netscript/agent/drift.jsonl` under the project root containing `timestamp`, `resource`, `summary`, optional `details`, and the attached evidence receipt.\n- **Failure modes**: If no receipt exists, if the receipt is older than 15 minutes, or if the receipt recorded a non-zero exit status, `record_drift` refuses with structured error code `diagnostic_evidence_required`.\n- **Dry-run / Preview**: Inspecting receipts or running `doctor` / telemetry tools previews current diagnostic state without mutating `drift.jsonl`.\n\n## Embedding as a library\n\nTo run the public stdio composition from your own Deno entrypoint:\n\n```ts\nimport { runMcpStdioServer } from \'@netscript/mcp/cli\';\n\nawait runMcpStdioServer({\n projectRoot: Deno.cwd(),\n // Omit docsRoot to use the package-embedded corpus, or select a filesystem corpus explicitly.\n docsRoot: Deno.env.get(\'NETSCRIPT_DOCS_ROOT\'),\n});\n```\n\n`runMcpStdioServer` owns the newline-delimited stdio transport. It shuts down when the host closes\nstdin or terminates the process; callers do not need to reach into an internal transport API.\n\n## Public surface\n\nThree entrypoints carry the package:\n\n| Entry | What it gives you |\n| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |\n| `.` | Tool contracts and schemas, the tool registry, protocol runner, service endpoint directory ports, and default adapters |\n| `./cli` | The executable composition plus every export from `.`, including the service endpoint directory surface |\n| `./openapi-projection` | Pure OpenAPI operation indexing and schema projections, with no discovery, filesystem, network, or runtime work |\n\nThe projection subpath accepts an already-loaded OpenAPI document. It keeps discovery and I/O at\nthe caller\'s boundary:\n\n```ts\nimport { indexOpenApiOperations } from \'@netscript/mcp/openapi-projection\';\n\nconst index = indexOpenApiOperations(openApiDocument);\n\nEvery tool flow depends on a port interface, so embedders and tests supply their own adapters and\nassert against the published schemas. The always-current symbol list is\n[`deno doc jsr:@netscript/mcp@`](https://jsr.io/@netscript/mcp/doc) (pin `` on the\npre-release line, as above).\n\n### Discover service OpenAPI endpoints\n\nEmbedders can compose the four discovery sources and bounded network probe without importing an\nOpenAPI projection layer:\n\n```ts\nimport { createServiceEndpointDirectory } from \'@netscript/mcp\';\n\nconst endpoints = createServiceEndpointDirectory({\n projectRoot: Deno.cwd(),\n});\n\nconst { entries, sources } = await endpoints.list();\n```\n\nThe effective per-service precedence is `override > aspire-cli > run-manifest > appsettings`. Every\nsource remains visible as `used`, `absent`, or `failed`; a failed Aspire CLI query or a stale\nmanifest is never rendered as healthy absence. The Aspire adapter brackets its machine-readable\n`describe` query with `ps` snapshots: the exact real AppHost path must belong to the project, its\nprocess identity must remain stable across the read, and any executable resource working directory\nmust remain inside that same real project root. CLI absence, non-zero exit, identity drift, and\npartial JSON are distinct failed source rows. Benign CLI banners, trailers, and casing changes are\naccepted without weakening those checks. The manifest at `.netscript/run/endpoints.json` is\neligible only when its real project root and `runId` match the supplied current run. `appHostPath`\ndefaults to `./aspire/apphost.mts`; override it when the active AppHost lives elsewhere. Supply\n`expectedRunId` only when the host owns the current AppHost run token; without that identity proof,\na present run manifest is reported as failed and does not contribute endpoints.\n\nExplicit operator endpoints and exclusions live only in the S5-owned subsection of\n`.netscript/agent-mcp.json`; sibling settings are ignored:\n\n```json\n{\n "introspection": {\n "serviceEndpoints": {\n "orders": "https://orders.example.test"\n },\n "excludeServices": ["internal-admin"]\n }\n}\n```\n\nExclusions are applied before network access. Other rows report `running`, `not_running`,\n`spec_unavailable`, or `identity_mismatch`; parsed OpenAPI is retained as opaque JSON for a later\nconsumer. Probes do not send credentials or follow redirects. A 401/403 explains how to expose only\nthe OpenAPI route anonymously or supply a reachable public spec URL. A running service must return\nJSON containing its selected service name, for example `{ "service": "orders" }`, from its selected\nbase path; this second request prevents a reused port from being mistaken for the intended service.\n\nThe default library composition needs `--allow-read` for carriers and real-path checks,\n`--allow-run` for `aspire ps` plus `aspire describe`, and `--allow-net` for bounded spec/identity requests. Tests and\ncustom hosts can replace every source and the probe through `ServiceEndpointDirectoryOptions`.\n\n## Configuration at a glance\n\n- **Telemetry endpoint discovery** (tools and `doctor`): explicit `--endpoint`, then\n `NETSCRIPT_TELEMETRY_ENDPOINT`, then `ASPIRE_DASHBOARD_PORT`, then `http://localhost:18888`.\n- **Docs corpus**: by default the docs tools index the documentation shipped with the installed\n package; set `--docs-root ` (or `NETSCRIPT_DOCS_ROOT`) to serve a project or site corpus\n instead.\n- **Service endpoint discovery** (library surface): `.netscript/agent-mcp.json` override, then the\n Aspire CLI machine-readable query, then an identity-bound run manifest, then\n `aspire/appsettings.json`; lower-priority disagreements remain visible as conflicts.\n- **Command policy**: the shipped default allows the prefixes\n `db init|generate|migrate|seed|status|introspect`, `generate`, `contract`, `service list`,\n `plugin install|list|sync|doctor`, and `ui:add|ui:init|ui:list|ui:update`, and denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove` — deny beats allow, anything\n unmatched is denied. Embedders can pass their own policy.\n\nThe full flag reference, policy table, and composition options are on the docs site.\n\n## Docs\n\n- **MCP reference — the 17-tool field overview, policy, and exports**:\n [rickylabs.github.io/netscript/reference/mcp/](https://rickylabs.github.io/netscript/reference/mcp/)\n- **Agent tooling — install, flags, troubleshooting, CLI × skills × MCP**:\n [rickylabs.github.io/netscript/capabilities/agent-tooling/](https://rickylabs.github.io/netscript/capabilities/agent-tooling/)\n- **API docs on JSR**: [jsr.io/@netscript/mcp/doc](https://jsr.io/@netscript/mcp/doc)\n\n## Compatibility\n\nThe **server** requires Deno 2.9+ (both entrypoints use `Deno.*` APIs); Node.js and Bun are not\nsupported as server runtimes. The **client** side is unconstrained: any MCP-capable host — Claude\nCode, VS Code, and others — only has to spawn the process and speak JSON-RPC over stdio. The\nexecutable needs `--allow-env`, `--allow-net`, `--allow-read`, and `--allow-run`; the `netscript`\nbinary grants these at its edge. The server never returns project source, environment-variable\nvalues, credentials, or secrets.\n\n## License\n\nApache-2.0 — see [LICENSE](https://github.com/rickylabs/netscript/blob/main/LICENSE). Published to\nJSR with cryptographically verified provenance.\n'; diff --git a/packages/mcp/tests/service-endpoint-source-fixtures.ts b/packages/mcp/tests/service-endpoint-source-fixtures.ts index 88da61d377..d8413d7ea4 100644 --- a/packages/mcp/tests/service-endpoint-source-fixtures.ts +++ b/packages/mcp/tests/service-endpoint-source-fixtures.ts @@ -5,10 +5,12 @@ export const ASPIRE_DESCRIBE_FIXTURE = `Aspire CLI 13.4.6 { "name": "users-yqkcrqst", "displayName": "users", + "properties": { "executable.workDir": "/project/services/users" }, "urls": [{ "name": "http", "url": "http://localhost:43127" }] }, { "name": "billing-abcdefgh", + "properties": { "executable.workDir": "/project/services/billing" }, "urls": [{ "name": "http", "url": "http://127.0.0.2:43128" }] }, { diff --git a/packages/mcp/tests/service-endpoint-sources_test.ts b/packages/mcp/tests/service-endpoint-sources_test.ts index d1c8ddf08b..ecf68bdd53 100644 --- a/packages/mcp/tests/service-endpoint-sources_test.ts +++ b/packages/mcp/tests/service-endpoint-sources_test.ts @@ -164,28 +164,26 @@ Deno.test('torn manifest remains failed while healthy appsettings remains indepe Deno.test('Aspire CLI source uses the 13.4 machine query and parses banner-prefixed resources', async () => { const calls: Array<{ command: string; args: readonly string[] }> = []; + let invocation = 0; const source = new AspireCliEndpointSource({ execute: (command, args) => { calls.push({ command, args }); - return Promise.resolve({ code: 0, stdout: ASPIRE_DESCRIBE_FIXTURE, stderr: '' }); + invocation++; + return Promise.resolve( + invocation === 2 ? { code: 0, stdout: ASPIRE_DESCRIBE_FIXTURE, stderr: '' } : { + code: 0, + stdout: '[{"appHostPath":"/project/apphost.mts","appHostPid":4312}]', + stderr: '', + }, + ); }, + realPath: (path) => Promise.resolve(path), }); const outcome = await source.read({ projectRoot: '/project', appHostPath: '/project/apphost.mts', }); - assertEquals(calls, [{ - command: 'aspire', - args: [ - 'describe', - '--apphost', - '/project/apphost.mts', - '--format', - 'Json', - '--non-interactive', - '--nologo', - ], - }]); + assertEquals(calls.map(({ args }) => args[0]), ['ps', 'describe', 'ps']); assertEquals(outcome, { source: 'aspire-cli', outcome: 'used', @@ -209,19 +207,65 @@ Deno.test('Aspire CLI source uses the 13.4 machine query and parses banner-prefi Deno.test('Aspire CLI absence, non-zero exit, and parse failure are explicit failed rows', async () => { const projectRoot = '/fixture'; - const missing = await new AspireCliEndpointSource({ command: 'missing-aspire-fixture' }).read({ - projectRoot, - }); + const missing = await new AspireCliEndpointSource({ + command: 'missing-aspire-fixture', + realPath: (path) => Promise.resolve(path), + }).read({ projectRoot }); assertEquals(failedCode(missing), 'command_not_found'); const nonZero = await new AspireCliEndpointSource({ execute: () => Promise.resolve({ code: 12, stdout: '', stderr: 'dashboard unavailable' }), + realPath: (path) => Promise.resolve(path), }).read({ projectRoot }); assertEquals(failedCode(nonZero), 'command_failed'); if (nonZero.outcome === 'failed') assertStringIncludes(nonZero.reason, 'exited 12'); const malformed = await new AspireCliEndpointSource({ execute: () => Promise.resolve({ code: 0, stdout: 'No running AppHost found', stderr: '' }), + realPath: (path) => Promise.resolve(path), }).read({ projectRoot }); assertEquals(failedCode(malformed), 'parse_failed'); }); + +Deno.test('Aspire CLI accepts casing drift and bounded trailing output while preserving identity', async () => { + const outputs = [ + 'Aspire 14\n{"Items":[{"AppHostPath":"/project/aspire/apphost.mts","AppHostPid":"run-7"}]}\ndone', + 'notice\n{"Resources":[{"DisplayName":"users","Endpoints":[{"Url":"http://localhost:41234"}],"Properties":{"WorkDir":"/project/services/users"}}]}\ntelemetry', + '{"appHosts":[{"appHostPath":"/project/aspire/apphost.mts","appHostPid":"run-7"}]}', + ]; + const source = new AspireCliEndpointSource({ + execute: () => Promise.resolve({ code: 0, stdout: outputs.shift()!, stderr: '' }), + realPath: (path) => Promise.resolve(path), + }); + const outcome = await source.read({ projectRoot: '/project' }); + assertEquals(outcome.outcome, 'used'); + assertEquals(outcome.candidates[0]?.baseUrl, 'http://127.0.0.1:41234'); +}); + +Deno.test('Aspire CLI rejects torn output, foreign resources, and AppHost restart races', async () => { + const ps = (runId: string) => + JSON.stringify([{ appHostPath: '/project/aspire/apphost.mts', appHostPid: runId }]); + const describe = (workDir: string) => + JSON.stringify({ + resources: [{ + displayName: 'users', + urls: [{ url: 'http://localhost:41234' }], + properties: { 'executable.workDir': workDir }, + }], + }); + const read = async (outputs: string[]) => + await new AspireCliEndpointSource({ + execute: () => Promise.resolve({ code: 0, stdout: outputs.shift()!, stderr: '' }), + realPath: (path) => Promise.resolve(path), + }).read({ projectRoot: '/project' }); + + assertEquals(failedCode(await read([ps('7'), '{"resources":[', ps('7')])), 'parse_failed'); + assertEquals( + failedCode(await read([ps('7'), describe('/foreign/services/users'), ps('7')])), + 'project_root_mismatch', + ); + assertEquals( + failedCode(await read([ps('7'), describe('/project/services/users'), ps('8')])), + 'run_id_mismatch', + ); +}); From 16fc9528c9b319b0559510067456e879a42cc764 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Tue, 4 Aug 2026 11:10:58 +0200 Subject: [PATCH 3/6] test(mcp): prove live Aspire endpoint discovery --- .../context-pack.md | 18 ++-- .../drift.md | 22 +++++ .../leak-report.md | 7 ++ .../run-resources.json | 10 ++ .../worklog.md | 9 +- .../gates/scaffold/runtime-gates.ts | 18 ++++ .../scaffold/verify-mcp-endpoint-directory.ts | 20 ++++ packages/cli/e2e/src/domain/cli-surface.ts | 1 + .../e2e/suites/scaffold/capability-suites.ts | 1 + .../builders/runtime-gates_test.ts | 29 ++++++ packages/mcp/README.md | 93 +++++++++++-------- .../aspire-cli-endpoint-source.ts | 19 +++- packages/mcp/src/publish-assets.generated.ts | 2 +- .../tests/service-endpoint-sources_test.ts | 2 +- 14 files changed, 198 insertions(+), 53 deletions(-) create mode 100644 .llm/runs/feat-openapi-mcp-manifest-cli--1133/leak-report.md create mode 100644 .llm/runs/feat-openapi-mcp-manifest-cli--1133/run-resources.json create mode 100644 packages/cli/e2e/src/application/gates/scaffold/verify-mcp-endpoint-directory.ts diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md index 81e2f90874..48b136688c 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md @@ -12,8 +12,9 @@ ## Current State -The identity-bound F1(b) adapter slice is implemented and its package/JSR gates pass. Baseline -verification still owns the AppHost slot; no expensive gate has started here. +The identity-bound F1(b) adapter and scaffold named gate are implemented. A real proxy collision +proved the identity guard and added allocated target-PORT selection. Package/static gates pass; +fresh branch CI must provide the final canonical runtime verdict. ## Completed @@ -23,15 +24,17 @@ verification still owns the AppHost slot; no expensive gate has started here. - Adapter fixtures cover CLI absence, command failure, benign format drift, torn output, foreign project resources, and AppHost restart races. - Package check/tests, scoped lint/fmt, quality/architecture, docs, and publish dry-run gates pass. +- Canonical S7 negative case rejected a foreign service on the fixed proxy port. ## In Progress -- Commit/push slice 2 and queue the serialized scaffold runtime proof. +- Commit/push S3, then watch fresh branch CI and composed evaluation. ## Next Steps -1. Re-check S6 and AppHost slot, then run serialized scaffold runtime evidence. -2. Push explicit refspec, maintain draft PR phase trail, and hand to composed evaluation. +1. Push explicit refspec and watch fresh scaffold-runtime CI. +2. Update acceptance evidence/DoD only if the named gate passes. +3. Hand to composed implementation evaluation. ## Key Decisions @@ -50,6 +53,7 @@ verification still owns the AppHost slot; no expensive gate has started here. | `packages/mcp/src/ports/service-endpoint-directory-port.ts` | changed | finite `run_id_mismatch` failure code | | `packages/mcp/tests/service-endpoint-*` | changed | negative and drift fixtures | | `packages/mcp/README.md` + generated asset | changed | production behavior and permissions | +| `packages/cli/e2e/**` | changed/new | named live directory gate wired into scaffold.runtime | ## Gates @@ -57,8 +61,8 @@ verification still owns the AppHost slot; no expensive gate has started here. | --- | --- | --- | | Static | pass | check + scoped lint/fmt | | Fitness | pass | focused/package tests, quality scan, arch check, JSR dry run | -| Runtime | queued | baseline owns AppHost slot | -| Consumer | queued | directory fixture unless S6 lands | +| Runtime | pending fresh CI | local attempts hit unrelated baseline failures; cleanup clean | +| Consumer | negative case pass / positive pending | named gate proved foreign-port refusal; target-port fix awaits CI | ## Open Questions diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md index 4562db9f12..7bee5f302e 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md @@ -21,3 +21,25 @@ - **Action:** accept - **Evidence:** `.llm/harness/workflow/milestone-run.md` +## 2026-08-04 — real Aspire proxy collision required target-port selection + +- **What:** A live scaffold described fixed proxy URL `127.0.0.1:3001` while that port belonged to a + foreign `products` process; the resource's allocated executable `PORT` was the trustworthy live target. +- **Source:** Canonical `scaffold.runtime` named gate `behavior.mcp-endpoint-directory`. +- **Expected:** The first declared HTTP URL would resolve to the described service. +- **Actual:** Identity probe correctly returned `identity_mismatch`; adapter selection was hardened to + prefer the described executable target port without weakening project/run/service identity checks. +- **Severity:** significant +- **Action:** fix +- **Evidence:** focused multi-port fixture plus scaffold output recorded in `worklog.md`. + +## 2026-08-04 — local full gate blocked by unrelated runtime health + +- **What:** Serialized canonical attempts did not reach a green suite after S7 was registered. +- **Source:** `scaffold.runtime --cleanup --format pretty`. +- **Expected:** All baseline runtime resources healthy, then S7 assertion passes. +- **Actual:** Attempts stopped at existing users DB-health aggregation or workers-api readiness; cleanup + passed. The one S7 execution fired its negative identity case and drove the target-port fix. +- **Severity:** minor +- **Action:** defer to fresh branch CI verdict +- **Evidence:** `worklog.md`; GitHub scaffold-runtime was green on the prior adapter commit. diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/leak-report.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/leak-report.md new file mode 100644 index 0000000000..d3a4a46a48 --- /dev/null +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/leak-report.md @@ -0,0 +1,7 @@ +# Run resource leak report + +Generated: 2026-08-04T08:39:53.743Z +Worktree: `/home/codex/repos/ns005-s7` +Aspire probe: ok +Docker probe: ok +No surviving Aspire resources found. diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/run-resources.json b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/run-resources.json new file mode 100644 index 0000000000..424dbff9b1 --- /dev/null +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/run-resources.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 2, + "worktreeRoot": "/home/codex/repos/ns005-s7", + "appHosts": [], + "containers": [], + "ownedRoots": [ + "/home/codex/repos/ns005-s7/.llm/tmp/cli-e2e/plugin-smoke-20260804-093556", + "/home/codex/repos/ns005-s7/.llm/tmp/cli-e2e" + ] +} diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md index 1e0424b069..6020d5a108 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md @@ -59,6 +59,11 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | 2026-08-04 | 2 | review | Split spawn and output parsing from the 370-line draft; final files are 36/282/64 lines. | | 2026-08-04 | 2 | live fault proof | A removed baseline AppHost returned `run_id_mismatch` with zero candidates; a current baseline AppHost returned `used`. | | 2026-08-04 | 2 | reconcile | S6 still open; no E2E tool-path dependency absorbed. AppHost gate remains serialized. | +| 2026-08-04 | 3 | interrupted gate | First canonical pass was killed by the Codex daemon update during AppHost start. Ownership reporter found and teardown removed two run-owned containers; no foreign resources touched. | +| 2026-08-04 | 3 | serialization | A retry was refused by the suite lease held by `ns005-ports`; queued until its PID exited. | +| 2026-08-04 | 3 | negative E2E | Named MCP gate rejected fixed proxy port 3001 because it identified as foreign service `products`; no false live row escaped. | +| 2026-08-04 | 3 | hardening | Adapter now prefers the real CLI-described executable target `PORT`, retaining declared URL host/protocol normalization. | +| 2026-08-04 | 3 | baseline failures | Full one-pass attempts later stopped at existing `behavior.service-health` DB health and `runtime.wait.workers-api` gates before S7. Cleanup passed and leak checks found no survivors. | ## Decisions @@ -102,7 +107,9 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | AppHost serialization | PASS | `aspire ps --format Json` | Baseline owns slot; this run did not start resources. | | live stale-run refusal | PASS | direct adapter read | removed AppHost produced `run_id_mismatch`, no candidates | | live current-run query | PASS | direct adapter read | current baseline DB-operation AppHost produced `used` | -| `scaffold.runtime` | NOT_RUN | queued | wait for baseline slot release | +| `scaffold.runtime` infrastructure | FAIL (baseline) | canonical one-pass command | independent pre-S7 failures: users DB-health aggregate; workers-api timeout | +| `behavior.mcp-endpoint-directory` negative case | PASS | canonical one-pass named gate | contested proxy identified as `products`; adapter returned `identity_mismatch` | +| `behavior.mcp-endpoint-directory` target-port case | PENDING_CI | fresh branch run | local static fixture passes; host slot is now held by foreign `wave5-deepseek` AppHost | ### Consumer Gates diff --git a/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts b/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts index 50e82c5598..5f77f284e0 100644 --- a/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts +++ b/packages/cli/e2e/src/application/gates/scaffold/runtime-gates.ts @@ -216,6 +216,24 @@ export function createRuntimeGates( ], (context) => context.project.projectRoot, ), + commandGate( + GATE.BEHAVIOR_MCP_ENDPOINT_DIRECTORY, + 'Resolve live service ports through MCP Aspire adapter', + GATE_PHASE.BEHAVIOR, + (context) => [ + 'deno', + 'run', + '--config', + `${context.project.repoRoot}/packages/mcp/deno.json`, + '--allow-read', + '--allow-run=aspire', + '--allow-net=127.0.0.1,localhost', + `${context.project.repoRoot}/packages/cli/e2e/src/application/gates/scaffold/verify-mcp-endpoint-directory.ts`, + context.project.projectRoot, + context.project.appHost, + ], + (context) => context.project.projectRoot, + ), commandGate( GATE.BEHAVIOR_SERVICE_HEALTH, 'Users service health', diff --git a/packages/cli/e2e/src/application/gates/scaffold/verify-mcp-endpoint-directory.ts b/packages/cli/e2e/src/application/gates/scaffold/verify-mcp-endpoint-directory.ts new file mode 100644 index 0000000000..55c1662151 --- /dev/null +++ b/packages/cli/e2e/src/application/gates/scaffold/verify-mcp-endpoint-directory.ts @@ -0,0 +1,20 @@ +import { createServiceEndpointDirectory } from '../../../../../../mcp/mod.ts'; + +const [projectRoot, appHostPath] = Deno.args; +if (!projectRoot || !appHostPath) { + throw new Error('project root and AppHost path are required'); +} + +const result = await createServiceEndpointDirectory({ projectRoot, appHostPath }).list(); +const aspire = result.sources.find((source) => source.source === 'aspire-cli'); +if (aspire?.outcome !== 'used') { + throw new Error(`aspire-cli source was not used: ${JSON.stringify(aspire)}`); +} + +const users = result.entries.find((entry) => entry.name === 'users'); +if (users?.status !== 'running' || users.source !== 'aspire-cli') { + throw new Error( + `users endpoint did not resolve live through aspire-cli: ${JSON.stringify(users)}`, + ); +} +console.log(`aspire-cli resolved users at ${users.baseUrl}`); diff --git a/packages/cli/e2e/src/domain/cli-surface.ts b/packages/cli/e2e/src/domain/cli-surface.ts index 3097f84682..ed6e9c5eab 100644 --- a/packages/cli/e2e/src/domain/cli-surface.ts +++ b/packages/cli/e2e/src/domain/cli-surface.ts @@ -102,6 +102,7 @@ export const GATE = { BEHAVIOR_WORKERS_TRIGGER_HEALTH_JOB: 'behavior.workers-trigger-health-job', BEHAVIOR_WORKERS_EXECUTIONS: 'behavior.workers-executions', BEHAVIOR_SERVICE_HEALTH: 'behavior.service-health', + BEHAVIOR_MCP_ENDPOINT_DIRECTORY: 'behavior.mcp-endpoint-directory', BEHAVIOR_DB_STATUS_PRESERVES_APPHOST: 'behavior.db-status-preserves-apphost', BEHAVIOR_ENDPOINT_READINESS: 'behavior.endpoint-readiness', BEHAVIOR_SAGAS_HEALTH: 'behavior.sagas-health', diff --git a/packages/cli/e2e/suites/scaffold/capability-suites.ts b/packages/cli/e2e/suites/scaffold/capability-suites.ts index c536020398..85aa5321bd 100644 --- a/packages/cli/e2e/suites/scaffold/capability-suites.ts +++ b/packages/cli/e2e/suites/scaffold/capability-suites.ts @@ -94,6 +94,7 @@ const RUNTIME_GATES = [ GATE.BEHAVIOR_WORKERS_SEED, GATE.BEHAVIOR_WORKERS_TRIGGER_HEALTH_JOB, GATE.BEHAVIOR_WORKERS_EXECUTIONS, + GATE.BEHAVIOR_MCP_ENDPOINT_DIRECTORY, GATE.BEHAVIOR_SERVICE_HEALTH, GATE.BEHAVIOR_SAGAS_HEALTH, GATE.BEHAVIOR_SAGAS_LIST, diff --git a/packages/cli/e2e/tests/application/builders/runtime-gates_test.ts b/packages/cli/e2e/tests/application/builders/runtime-gates_test.ts index 1e0260b04f..0b23a0fc34 100644 --- a/packages/cli/e2e/tests/application/builders/runtime-gates_test.ts +++ b/packages/cli/e2e/tests/application/builders/runtime-gates_test.ts @@ -114,6 +114,35 @@ Deno.test('runtime gates include durable workers and sagas CLI parity', () => { assertEquals(gate.failureHint, undefined); }); +Deno.test('runtime gates prove MCP Aspire endpoint discovery against the live AppHost', () => { + const gate = createRuntimeGates().find((entry) => + entry.id === GATE.BEHAVIOR_MCP_ENDPOINT_DIRECTORY + ); + if (gate?.kind !== 'command') { + throw new Error('Expected MCP endpoint directory gate to be a command gate.'); + } + const context = { + project: { + repoRoot: '/repo', + projectRoot: '/workspace/app', + appHost: '/workspace/app/aspire/apphost.mts', + }, + } as RunContext; + assertEquals(gate.cwd(context), '/workspace/app'); + assertEquals(gate.command(context), [ + 'deno', + 'run', + '--config', + '/repo/packages/mcp/deno.json', + '--allow-read', + '--allow-run=aspire', + '--allow-net=127.0.0.1,localhost', + '/repo/packages/cli/e2e/src/application/gates/scaffold/verify-mcp-endpoint-directory.ts', + '/workspace/app', + '/workspace/app/aspire/apphost.mts', + ]); +}); + Deno.test('AI chat route gate captures generated registry import failures', () => { const gate = createRuntimeGates().find((entry) => entry.id === GATE.BEHAVIOR_AI_CHAT_ROUTE); if (gate?.kind !== 'command') { diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 90d0645a97..f04ca30f30 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -133,21 +133,21 @@ results, and `get_run` returns a structured `run_not_found` error the agent can ## Tool catalog -| Tool | Required input | Bounded result | -| ----------------------------- | ----------------- | ---------------------------------------------------------------------------- | -| `get_app_status` | — | Health verdict, counts, per-domain summaries | -| `list_runs` | — | Recent executions filtered by domain, status, service, time | -| `get_run` | `id` | One correlated execution with bounded spans and logs | -| `get_recent_errors` | — | Recent errors grouped by service and domain | -| `get_last_job_result` | — | The latest matching job outcome | -| `analyze_service_performance` | `service` | Duration percentiles, throughput, error rate | -| `analyze_db_bottlenecks` | — | Ranked database and KV operations | -| `doctor` | — | Telemetry, Aspire, wiring, and plugin checks; suggested fixes on problems | -| `search_docs` | `query` | Ranked public-document matches with snippets | -| `list_docs` | — | Public-document summaries | -| `get_doc` | `slug` | One public document, or one named section of it | -| `list_commands` | — | Live CLI command descriptors | -| `execute_command` | `command` | Exit code, duration, and bounded output tail; structured denial when blocked | +| Tool | Required input | Bounded result | +| ----------------------------- | --------------------- | ---------------------------------------------------------------------------- | +| `get_app_status` | — | Health verdict, counts, per-domain summaries | +| `list_runs` | — | Recent executions filtered by domain, status, service, time | +| `get_run` | `id` | One correlated execution with bounded spans and logs | +| `get_recent_errors` | — | Recent errors grouped by service and domain | +| `get_last_job_result` | — | The latest matching job outcome | +| `analyze_service_performance` | `service` | Duration percentiles, throughput, error rate | +| `analyze_db_bottlenecks` | — | Ranked database and KV operations | +| `doctor` | — | Telemetry, Aspire, wiring, and plugin checks; suggested fixes on problems | +| `search_docs` | `query` | Ranked public-document matches with snippets | +| `list_docs` | — | Public-document summaries | +| `get_doc` | `slug` | One public document, or one named section of it | +| `list_commands` | — | Live CLI command descriptors | +| `execute_command` | `command` | Exit code, duration, and bounded output tail; structured denial when blocked | | `record_drift` | `resource`, `summary` | Evidence-gated drift entry appended to project drift log | | `list_api_services` | — | Discovered services, live spec status, source outcomes, and operation counts | | `list_service_operations` | `service` | Bounded OpenAPI operation rows with honest truncation metadata | @@ -160,13 +160,22 @@ contracts are published as `TOOL_INPUT_SCHEMAS` / `TOOL_OUTPUT_SCHEMAS` and retu ## Record drift -`record_drift` is an evidence-gated mutating tool that records verified architecture or runtime drift into `.netscript/agent/drift.jsonl`. - -- **Required evidence**: Requires a fresh successful diagnostic receipt (timestamped within 15 minutes, `exitStatus: 0`) for the target resource. Receipts are automatically produced when calling `doctor`, telemetry tools, or `netscript plugin doctor --resource `. -- **Target & Scope**: The `resource` argument targets a specific plugin, service, or `'project'`. Receipts live at `.netscript/agent/diagnostics/.json`. -- **Mutation behavior**: Appends a single JSON line to `.netscript/agent/drift.jsonl` under the project root containing `timestamp`, `resource`, `summary`, optional `details`, and the attached evidence receipt. -- **Failure modes**: If no receipt exists, if the receipt is older than 15 minutes, or if the receipt recorded a non-zero exit status, `record_drift` refuses with structured error code `diagnostic_evidence_required`. -- **Dry-run / Preview**: Inspecting receipts or running `doctor` / telemetry tools previews current diagnostic state without mutating `drift.jsonl`. +`record_drift` is an evidence-gated mutating tool that records verified architecture or runtime +drift into `.netscript/agent/drift.jsonl`. + +- **Required evidence**: Requires a fresh successful diagnostic receipt (timestamped within 15 + minutes, `exitStatus: 0`) for the target resource. Receipts are automatically produced when + calling `doctor`, telemetry tools, or `netscript plugin doctor --resource `. +- **Target & Scope**: The `resource` argument targets a specific plugin, service, or `'project'`. + Receipts live at `.netscript/agent/diagnostics/.json`. +- **Mutation behavior**: Appends a single JSON line to `.netscript/agent/drift.jsonl` under the + project root containing `timestamp`, `resource`, `summary`, optional `details`, and the attached + evidence receipt. +- **Failure modes**: If no receipt exists, if the receipt is older than 15 minutes, or if the + receipt recorded a non-zero exit status, `record_drift` refuses with structured error code + `diagnostic_evidence_required`. +- **Dry-run / Preview**: Inspecting receipts or running `doctor` / telemetry tools previews current + diagnostic state without mutating `drift.jsonl`. ## Embedding as a library @@ -189,16 +198,16 @@ stdin or terminates the process; callers do not need to reach into an internal t Three entrypoints carry the package: -| Entry | What it gives you | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `.` | Tool contracts and schemas, the tool registry, protocol runner, service endpoint directory ports, and default adapters | -| `./cli` | The executable composition plus every export from `.`, including the service endpoint directory surface | -| `./openapi-projection` | Pure OpenAPI operation indexing and schema projections, with no discovery, filesystem, network, or runtime work | +| Entry | What it gives you | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `.` | Tool contracts and schemas, the tool registry, protocol runner, service endpoint directory ports, and default adapters | +| `./cli` | The executable composition plus every export from `.`, including the service endpoint directory surface | +| `./openapi-projection` | Pure OpenAPI operation indexing and schema projections, with no discovery, filesystem, network, or runtime work | -The projection subpath accepts an already-loaded OpenAPI document. It keeps discovery and I/O at -the caller's boundary: +The projection subpath accepts an already-loaded OpenAPI document. It keeps discovery and I/O at the +caller's boundary: -```ts +````ts import { indexOpenApiOperations } from '@netscript/mcp/openapi-projection'; const index = indexOpenApiOperations(openApiDocument); @@ -221,20 +230,23 @@ const endpoints = createServiceEndpointDirectory({ }); const { entries, sources } = await endpoints.list(); -``` +```` The effective per-service precedence is `override > aspire-cli > run-manifest > appsettings`. Every source remains visible as `used`, `absent`, or `failed`; a failed Aspire CLI query or a stale manifest is never rendered as healthy absence. The Aspire adapter brackets its machine-readable `describe` query with `ps` snapshots: the exact real AppHost path must belong to the project, its process identity must remain stable across the read, and any executable resource working directory -must remain inside that same real project root. CLI absence, non-zero exit, identity drift, and -partial JSON are distinct failed source rows. Benign CLI banners, trailers, and casing changes are -accepted without weakening those checks. The manifest at `.netscript/run/endpoints.json` is -eligible only when its real project root and `runId` match the supplied current run. `appHostPath` -defaults to `./aspire/apphost.mts`; override it when the active AppHost lives elsewhere. Supply -`expectedRunId` only when the host owns the current AppHost run token; without that identity proof, -a present run manifest is reported as failed and does not contribute endpoints. +must remain inside that same real project root. For executable services, the adapter prefers the +allocated target `PORT` from the described resource environment over a fixed proxy URL; this keeps +discovery live when a stale foreign process occupies the requested proxy port. CLI absence, non-zero +exit, identity drift, and partial JSON are distinct failed source rows. Benign CLI banners, +trailers, and casing changes are accepted without weakening those checks. The manifest at +`.netscript/run/endpoints.json` is eligible only when its real project root and `runId` match the +supplied current run. `appHostPath` defaults to `./aspire/apphost.mts`; override it when the active +AppHost lives elsewhere. Supply `expectedRunId` only when the host owns the current AppHost run +token; without that identity proof, a present run manifest is reported as failed and does not +contribute endpoints. Explicit operator endpoints and exclusions live only in the S5-owned subsection of `.netscript/agent-mcp.json`; sibling settings are ignored: @@ -258,8 +270,9 @@ JSON containing its selected service name, for example `{ "service": "orders" }` base path; this second request prevents a reused port from being mistaken for the intended service. The default library composition needs `--allow-read` for carriers and real-path checks, -`--allow-run` for `aspire ps` plus `aspire describe`, and `--allow-net` for bounded spec/identity requests. Tests and -custom hosts can replace every source and the probe through `ServiceEndpointDirectoryOptions`. +`--allow-run` for `aspire ps` plus `aspire describe`, and `--allow-net` for bounded spec/identity +requests. Tests and custom hosts can replace every source and the probe through +`ServiceEndpointDirectoryOptions`. ## Configuration at a glance diff --git a/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-endpoint-source.ts b/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-endpoint-source.ts index 230a6716a9..ab40def440 100644 --- a/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-endpoint-source.ts +++ b/packages/mcp/src/infrastructure/service-endpoints/aspire-cli-endpoint-source.ts @@ -133,7 +133,7 @@ export class AspireCliEndpointSource implements EndpointSourcePort { for (const rawResource of resources) { if (!isAspireRecord(rawResource)) continue; const name = resourceName(rawResource); - const baseUrl = firstHttpUrl(aspireField(rawResource, 'urls', 'endpoints')); + const baseUrl = resourceHttpUrl(rawResource); if (!name || !baseUrl) continue; const workDir = resourceWorkDir(rawResource); if (workDir) { @@ -239,7 +239,8 @@ function resourceName(resource: Record): string | undefined { return name.replace(/-[a-z0-9]{8}$/i, ''); } -function firstHttpUrl(value: unknown): string | undefined { +function resourceHttpUrl(resource: Record): string | undefined { + const value = aspireField(resource, 'urls', 'endpoints'); if (!Array.isArray(value)) return undefined; const urls: string[] = []; for (const entry of value) { @@ -247,7 +248,19 @@ function firstHttpUrl(value: unknown): string | undefined { const normalized = normalizeDiscoveredEndpointUrl(rawUrl, false); if (normalized) urls.push(normalized); } - return urls.find((url) => url.startsWith('http://')) ?? urls[0]; + const declared = urls.find((url) => url.startsWith('http://')) ?? urls[0]; + if (!declared) return undefined; + + const environment = aspireField(resource, 'environment'); + const rawTargetPort = isAspireRecord(environment) ? aspireField(environment, 'PORT') : undefined; + const targetPort = typeof rawTargetPort === 'string' || typeof rawTargetPort === 'number' + ? Number(rawTargetPort) + : Number.NaN; + if (!Number.isInteger(targetPort) || targetPort < 1 || targetPort > 65_535) return declared; + const target = new URL(declared); + target.protocol = 'http:'; + target.port = String(targetPort); + return normalizeDiscoveredEndpointUrl(target.toString(), false) ?? declared; } function resourceWorkDir(resource: Record): string | undefined { diff --git a/packages/mcp/src/publish-assets.generated.ts b/packages/mcp/src/publish-assets.generated.ts index eec7100992..aad61c637a 100644 --- a/packages/mcp/src/publish-assets.generated.ts +++ b/packages/mcp/src/publish-assets.generated.ts @@ -6,4 +6,4 @@ export const MCP_PACKAGE_VERSION: string = '0.0.4'; /** Published MCP README embedded as the default documentation corpus. */ export const MCP_PACKAGE_README: string = - '# @netscript/mcp\n\n[![JSR](https://jsr.io/badges/@netscript/mcp)](https://jsr.io/@netscript/mcp)\n[![CI](https://github.com/rickylabs/netscript/actions/workflows/ci.yml/badge.svg)](https://github.com/rickylabs/netscript/actions/workflows/ci.yml)\n[![Docs](https://img.shields.io/badge/docs-rickylabs.github.io-blue)](https://rickylabs.github.io/netscript/)\n\n**The Model Context Protocol server for NetScript: 17 token-bounded tools that let a coding agent\nmonitor a running app, debug a correlated execution, read framework-semantic telemetry, run the\ndoctor, and search the docs — all over stdio.**\n\nPoint Claude Code or VS Code at a running NetScript app and the agent can ask _"is the app\nhealthy?"_, _"why did the last import job fail?"_, and _"what is slowing down `checkout`?"_ — and\nget compact, structured answers instead of raw logs. It can correlate one execution\'s spans, logs,\nand outcome by id; rank the queries hammering your database; and trigger allowlisted CLI commands\nthrough a default-deny policy. One command — `netscript agent init` — wires all of it into your\nagent host.\n\nGeneric observability tooling hands an agent raw spans and log lines and lets it burn its context\nwindow re-deriving structure the framework already knows. `@netscript/mcp` answers in NetScript\'s\nown vocabulary — jobs, sagas, triggers, streams, services — and bounds every result server-side, so\nthe agent gets percentiles, error rates, and ranked operations rather than the spans they were\ncomputed from. It reads the same OpenTelemetry data the Aspire dashboard shows you, and complements\nAspire\'s own MCP server: Aspire speaks resources and containers; this server speaks your app.\n\n## Why agents like it\n\n- **17 token-bounded tools** — every successful result is capped server-side (50 array items, 2,000\n characters per string) before it reaches the model; the analytics tools never return raw spans at\n all.\n- **Framework-semantic trace intelligence** — tools classify telemetry into `worker`, `saga`,\n `trigger`, `stream`, and `service` domains and correlate whole executions by id, because they\n understand the `netscript.*` attribute conventions the framework emits.\n- **Default-deny CLI gate** — `execute_command` matches commands against an ordered prefix policy;\n deny beats allow, anything unmatched is denied, and the shipped policy explicitly denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove`.\n- **One-command install** — `netscript agent init` detects your agent host, writes the MCP\n configuration, and installs the matching NetScript skills.\n- **Matched agent surface** — `netscript agent init` writes host configuration pinned to your\n installed CLI version and installs the skills that ship with that same release, so the tool\n catalog the agent sees comes from the release it runs.\n- **Zero npm MCP SDK** — a minimal newline-delimited JSON-RPC transport keeps the dependency graph\n lean and the lockfile stable.\n\n## Architecture\n\n```mermaid\nflowchart LR\n A["Agent host
(Claude Code, VS Code, ...)"] <-- "JSON-RPC / stdio" --> S["netscript agent mcp
17 tools · bounded results"]\n S --> T["Telemetry endpoint
(OTLP read model)"]\n S --> D["Docs corpus
(public Markdown)"]\n S --> P["Command policy
(default-deny allowlist)"]\n T --> R["Running NetScript app"]\n P --> C["netscript CLI"]\n C --> R\n```\n\nThe server is one third of the NetScript agent surface — the CLI is the hands, the skills are the\nplaybook, MCP is the eyes. It deliberately wraps the CLI rather than reimplementing it:\n`list_commands` reflects the live command tree, and `execute_command` shells the CLI through the\npolicy gate. MCP exists for what a shell cannot cheaply give an agent — bounded aggregation,\ncross-domain diagnostics, and documentation lookup.\n\n## Install\n\nMost users never import this package. Install the server into a project with the CLI:\n\n```bash\nnetscript agent init\n```\n\nThat detects your agent host and writes `.mcp.json` (Claude Code) and/or `.vscode/mcp.json` (VS\nCode) pointing at `netscript agent mcp`, and installs the NetScript skills shipped with your CLI\nrelease. Use `--host claude|vscode|all` to choose explicitly.\n\nTo embed the server in your own host process, add it as a library:\n\n```bash\ndeno add jsr:@netscript/mcp@\n```\n\nTo run the standalone stdio entrypoint directly when integrating another MCP host:\n\n```bash\ndeno x -A jsr:@netscript/mcp@/cli\n```\n\nPin `` to match your installed CLI; bare `jsr:@netscript/*` specifiers do not resolve on\nthe pre-release line, and `netscript agent init` writes the correct pinned form for you.\n\n## Quick example\n\n**1. Wire up an agent host.** From a NetScript project root:\n\n```bash\n$ netscript agent init\nInstalled NetScript agent integration for claude, vscode.\n```\n\nThe generated `.mcp.json` runs the server for this project — equivalent to:\n\n```json\n{\n "mcpServers": {\n "netscript": {\n "command": "deno",\n "args": [\n "run",\n "-A",\n "jsr:@netscript/cli@",\n "agent",\n "mcp",\n "--project-root",\n ""\n ]\n }\n }\n}\n```\n\n**2. Ask the agent.** With the app started, the agent turns questions into bounded tool calls:\n\n> **You:** Is the app healthy? Anything in the docs about telemetry?\n>\n> **Agent:** calls `get_app_status` →\n> `{"status": "…", "counts": {…}, "domains": [{"domain": "worker", …}, …]}` — a health verdict with\n> per-domain summaries, not a span dump. Calls `search_docs {"query": "telemetry"}` →\n> `{"count": 1, "matches": [{"slug": "mcp", "title": "@netscript/mcp", "snippet": "…", "score": 35}]}`,\n> then `get_doc` with the winning slug to read just the section it needs.\n\nWhen telemetry is unreachable, nothing crashes: `get_app_status` and the doctor\'s telemetry checks\nreport a structured `warn`/`fail` status, the list and analytics tools return their ordinary empty\nresults, and `get_run` returns a structured `run_not_found` error the agent can reason about.\n\n## Tool catalog\n\n| Tool | Required input | Bounded result |\n| ----------------------------- | ----------------- | ---------------------------------------------------------------------------- |\n| `get_app_status` | — | Health verdict, counts, per-domain summaries |\n| `list_runs` | — | Recent executions filtered by domain, status, service, time |\n| `get_run` | `id` | One correlated execution with bounded spans and logs |\n| `get_recent_errors` | — | Recent errors grouped by service and domain |\n| `get_last_job_result` | — | The latest matching job outcome |\n| `analyze_service_performance` | `service` | Duration percentiles, throughput, error rate |\n| `analyze_db_bottlenecks` | — | Ranked database and KV operations |\n| `doctor` | — | Telemetry, Aspire, wiring, and plugin checks; suggested fixes on problems |\n| `search_docs` | `query` | Ranked public-document matches with snippets |\n| `list_docs` | — | Public-document summaries |\n| `get_doc` | `slug` | One public document, or one named section of it |\n| `list_commands` | — | Live CLI command descriptors |\n| `execute_command` | `command` | Exit code, duration, and bounded output tail; structured denial when blocked |\n| `record_drift` | `resource`, `summary` | Evidence-gated drift entry appended to project drift log |\n| `list_api_services` | — | Discovered services, live spec status, source outcomes, and operation counts |\n| `list_service_operations` | `service` | Bounded OpenAPI operation rows with honest truncation metadata |\n| `get_operation_schema` | `service`, `operation` | Request, response, and error views plus an unauthenticated curl template |\n\nA top-level input/result field overview for every tool is on the\n[MCP reference](https://rickylabs.github.io/netscript/reference/mcp/); the complete Standard Schema\ncontracts are published as `TOOL_INPUT_SCHEMAS` / `TOOL_OUTPUT_SCHEMAS` and returned by the live\n`tools/list`.\n\n## Record drift\n\n`record_drift` is an evidence-gated mutating tool that records verified architecture or runtime drift into `.netscript/agent/drift.jsonl`.\n\n- **Required evidence**: Requires a fresh successful diagnostic receipt (timestamped within 15 minutes, `exitStatus: 0`) for the target resource. Receipts are automatically produced when calling `doctor`, telemetry tools, or `netscript plugin doctor --resource `.\n- **Target & Scope**: The `resource` argument targets a specific plugin, service, or `\'project\'`. Receipts live at `.netscript/agent/diagnostics/.json`.\n- **Mutation behavior**: Appends a single JSON line to `.netscript/agent/drift.jsonl` under the project root containing `timestamp`, `resource`, `summary`, optional `details`, and the attached evidence receipt.\n- **Failure modes**: If no receipt exists, if the receipt is older than 15 minutes, or if the receipt recorded a non-zero exit status, `record_drift` refuses with structured error code `diagnostic_evidence_required`.\n- **Dry-run / Preview**: Inspecting receipts or running `doctor` / telemetry tools previews current diagnostic state without mutating `drift.jsonl`.\n\n## Embedding as a library\n\nTo run the public stdio composition from your own Deno entrypoint:\n\n```ts\nimport { runMcpStdioServer } from \'@netscript/mcp/cli\';\n\nawait runMcpStdioServer({\n projectRoot: Deno.cwd(),\n // Omit docsRoot to use the package-embedded corpus, or select a filesystem corpus explicitly.\n docsRoot: Deno.env.get(\'NETSCRIPT_DOCS_ROOT\'),\n});\n```\n\n`runMcpStdioServer` owns the newline-delimited stdio transport. It shuts down when the host closes\nstdin or terminates the process; callers do not need to reach into an internal transport API.\n\n## Public surface\n\nThree entrypoints carry the package:\n\n| Entry | What it gives you |\n| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |\n| `.` | Tool contracts and schemas, the tool registry, protocol runner, service endpoint directory ports, and default adapters |\n| `./cli` | The executable composition plus every export from `.`, including the service endpoint directory surface |\n| `./openapi-projection` | Pure OpenAPI operation indexing and schema projections, with no discovery, filesystem, network, or runtime work |\n\nThe projection subpath accepts an already-loaded OpenAPI document. It keeps discovery and I/O at\nthe caller\'s boundary:\n\n```ts\nimport { indexOpenApiOperations } from \'@netscript/mcp/openapi-projection\';\n\nconst index = indexOpenApiOperations(openApiDocument);\n\nEvery tool flow depends on a port interface, so embedders and tests supply their own adapters and\nassert against the published schemas. The always-current symbol list is\n[`deno doc jsr:@netscript/mcp@`](https://jsr.io/@netscript/mcp/doc) (pin `` on the\npre-release line, as above).\n\n### Discover service OpenAPI endpoints\n\nEmbedders can compose the four discovery sources and bounded network probe without importing an\nOpenAPI projection layer:\n\n```ts\nimport { createServiceEndpointDirectory } from \'@netscript/mcp\';\n\nconst endpoints = createServiceEndpointDirectory({\n projectRoot: Deno.cwd(),\n});\n\nconst { entries, sources } = await endpoints.list();\n```\n\nThe effective per-service precedence is `override > aspire-cli > run-manifest > appsettings`. Every\nsource remains visible as `used`, `absent`, or `failed`; a failed Aspire CLI query or a stale\nmanifest is never rendered as healthy absence. The Aspire adapter brackets its machine-readable\n`describe` query with `ps` snapshots: the exact real AppHost path must belong to the project, its\nprocess identity must remain stable across the read, and any executable resource working directory\nmust remain inside that same real project root. CLI absence, non-zero exit, identity drift, and\npartial JSON are distinct failed source rows. Benign CLI banners, trailers, and casing changes are\naccepted without weakening those checks. The manifest at `.netscript/run/endpoints.json` is\neligible only when its real project root and `runId` match the supplied current run. `appHostPath`\ndefaults to `./aspire/apphost.mts`; override it when the active AppHost lives elsewhere. Supply\n`expectedRunId` only when the host owns the current AppHost run token; without that identity proof,\na present run manifest is reported as failed and does not contribute endpoints.\n\nExplicit operator endpoints and exclusions live only in the S5-owned subsection of\n`.netscript/agent-mcp.json`; sibling settings are ignored:\n\n```json\n{\n "introspection": {\n "serviceEndpoints": {\n "orders": "https://orders.example.test"\n },\n "excludeServices": ["internal-admin"]\n }\n}\n```\n\nExclusions are applied before network access. Other rows report `running`, `not_running`,\n`spec_unavailable`, or `identity_mismatch`; parsed OpenAPI is retained as opaque JSON for a later\nconsumer. Probes do not send credentials or follow redirects. A 401/403 explains how to expose only\nthe OpenAPI route anonymously or supply a reachable public spec URL. A running service must return\nJSON containing its selected service name, for example `{ "service": "orders" }`, from its selected\nbase path; this second request prevents a reused port from being mistaken for the intended service.\n\nThe default library composition needs `--allow-read` for carriers and real-path checks,\n`--allow-run` for `aspire ps` plus `aspire describe`, and `--allow-net` for bounded spec/identity requests. Tests and\ncustom hosts can replace every source and the probe through `ServiceEndpointDirectoryOptions`.\n\n## Configuration at a glance\n\n- **Telemetry endpoint discovery** (tools and `doctor`): explicit `--endpoint`, then\n `NETSCRIPT_TELEMETRY_ENDPOINT`, then `ASPIRE_DASHBOARD_PORT`, then `http://localhost:18888`.\n- **Docs corpus**: by default the docs tools index the documentation shipped with the installed\n package; set `--docs-root ` (or `NETSCRIPT_DOCS_ROOT`) to serve a project or site corpus\n instead.\n- **Service endpoint discovery** (library surface): `.netscript/agent-mcp.json` override, then the\n Aspire CLI machine-readable query, then an identity-bound run manifest, then\n `aspire/appsettings.json`; lower-priority disagreements remain visible as conflicts.\n- **Command policy**: the shipped default allows the prefixes\n `db init|generate|migrate|seed|status|introspect`, `generate`, `contract`, `service list`,\n `plugin install|list|sync|doctor`, and `ui:add|ui:init|ui:list|ui:update`, and denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove` — deny beats allow, anything\n unmatched is denied. Embedders can pass their own policy.\n\nThe full flag reference, policy table, and composition options are on the docs site.\n\n## Docs\n\n- **MCP reference — the 17-tool field overview, policy, and exports**:\n [rickylabs.github.io/netscript/reference/mcp/](https://rickylabs.github.io/netscript/reference/mcp/)\n- **Agent tooling — install, flags, troubleshooting, CLI × skills × MCP**:\n [rickylabs.github.io/netscript/capabilities/agent-tooling/](https://rickylabs.github.io/netscript/capabilities/agent-tooling/)\n- **API docs on JSR**: [jsr.io/@netscript/mcp/doc](https://jsr.io/@netscript/mcp/doc)\n\n## Compatibility\n\nThe **server** requires Deno 2.9+ (both entrypoints use `Deno.*` APIs); Node.js and Bun are not\nsupported as server runtimes. The **client** side is unconstrained: any MCP-capable host — Claude\nCode, VS Code, and others — only has to spawn the process and speak JSON-RPC over stdio. The\nexecutable needs `--allow-env`, `--allow-net`, `--allow-read`, and `--allow-run`; the `netscript`\nbinary grants these at its edge. The server never returns project source, environment-variable\nvalues, credentials, or secrets.\n\n## License\n\nApache-2.0 — see [LICENSE](https://github.com/rickylabs/netscript/blob/main/LICENSE). Published to\nJSR with cryptographically verified provenance.\n'; + '# @netscript/mcp\n\n[![JSR](https://jsr.io/badges/@netscript/mcp)](https://jsr.io/@netscript/mcp)\n[![CI](https://github.com/rickylabs/netscript/actions/workflows/ci.yml/badge.svg)](https://github.com/rickylabs/netscript/actions/workflows/ci.yml)\n[![Docs](https://img.shields.io/badge/docs-rickylabs.github.io-blue)](https://rickylabs.github.io/netscript/)\n\n**The Model Context Protocol server for NetScript: 17 token-bounded tools that let a coding agent\nmonitor a running app, debug a correlated execution, read framework-semantic telemetry, run the\ndoctor, and search the docs — all over stdio.**\n\nPoint Claude Code or VS Code at a running NetScript app and the agent can ask _"is the app\nhealthy?"_, _"why did the last import job fail?"_, and _"what is slowing down `checkout`?"_ — and\nget compact, structured answers instead of raw logs. It can correlate one execution\'s spans, logs,\nand outcome by id; rank the queries hammering your database; and trigger allowlisted CLI commands\nthrough a default-deny policy. One command — `netscript agent init` — wires all of it into your\nagent host.\n\nGeneric observability tooling hands an agent raw spans and log lines and lets it burn its context\nwindow re-deriving structure the framework already knows. `@netscript/mcp` answers in NetScript\'s\nown vocabulary — jobs, sagas, triggers, streams, services — and bounds every result server-side, so\nthe agent gets percentiles, error rates, and ranked operations rather than the spans they were\ncomputed from. It reads the same OpenTelemetry data the Aspire dashboard shows you, and complements\nAspire\'s own MCP server: Aspire speaks resources and containers; this server speaks your app.\n\n## Why agents like it\n\n- **17 token-bounded tools** — every successful result is capped server-side (50 array items, 2,000\n characters per string) before it reaches the model; the analytics tools never return raw spans at\n all.\n- **Framework-semantic trace intelligence** — tools classify telemetry into `worker`, `saga`,\n `trigger`, `stream`, and `service` domains and correlate whole executions by id, because they\n understand the `netscript.*` attribute conventions the framework emits.\n- **Default-deny CLI gate** — `execute_command` matches commands against an ordered prefix policy;\n deny beats allow, anything unmatched is denied, and the shipped policy explicitly denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove`.\n- **One-command install** — `netscript agent init` detects your agent host, writes the MCP\n configuration, and installs the matching NetScript skills.\n- **Matched agent surface** — `netscript agent init` writes host configuration pinned to your\n installed CLI version and installs the skills that ship with that same release, so the tool\n catalog the agent sees comes from the release it runs.\n- **Zero npm MCP SDK** — a minimal newline-delimited JSON-RPC transport keeps the dependency graph\n lean and the lockfile stable.\n\n## Architecture\n\n```mermaid\nflowchart LR\n A["Agent host
(Claude Code, VS Code, ...)"] <-- "JSON-RPC / stdio" --> S["netscript agent mcp
17 tools · bounded results"]\n S --> T["Telemetry endpoint
(OTLP read model)"]\n S --> D["Docs corpus
(public Markdown)"]\n S --> P["Command policy
(default-deny allowlist)"]\n T --> R["Running NetScript app"]\n P --> C["netscript CLI"]\n C --> R\n```\n\nThe server is one third of the NetScript agent surface — the CLI is the hands, the skills are the\nplaybook, MCP is the eyes. It deliberately wraps the CLI rather than reimplementing it:\n`list_commands` reflects the live command tree, and `execute_command` shells the CLI through the\npolicy gate. MCP exists for what a shell cannot cheaply give an agent — bounded aggregation,\ncross-domain diagnostics, and documentation lookup.\n\n## Install\n\nMost users never import this package. Install the server into a project with the CLI:\n\n```bash\nnetscript agent init\n```\n\nThat detects your agent host and writes `.mcp.json` (Claude Code) and/or `.vscode/mcp.json` (VS\nCode) pointing at `netscript agent mcp`, and installs the NetScript skills shipped with your CLI\nrelease. Use `--host claude|vscode|all` to choose explicitly.\n\nTo embed the server in your own host process, add it as a library:\n\n```bash\ndeno add jsr:@netscript/mcp@\n```\n\nTo run the standalone stdio entrypoint directly when integrating another MCP host:\n\n```bash\ndeno x -A jsr:@netscript/mcp@/cli\n```\n\nPin `` to match your installed CLI; bare `jsr:@netscript/*` specifiers do not resolve on\nthe pre-release line, and `netscript agent init` writes the correct pinned form for you.\n\n## Quick example\n\n**1. Wire up an agent host.** From a NetScript project root:\n\n```bash\n$ netscript agent init\nInstalled NetScript agent integration for claude, vscode.\n```\n\nThe generated `.mcp.json` runs the server for this project — equivalent to:\n\n```json\n{\n "mcpServers": {\n "netscript": {\n "command": "deno",\n "args": [\n "run",\n "-A",\n "jsr:@netscript/cli@",\n "agent",\n "mcp",\n "--project-root",\n ""\n ]\n }\n }\n}\n```\n\n**2. Ask the agent.** With the app started, the agent turns questions into bounded tool calls:\n\n> **You:** Is the app healthy? Anything in the docs about telemetry?\n>\n> **Agent:** calls `get_app_status` →\n> `{"status": "…", "counts": {…}, "domains": [{"domain": "worker", …}, …]}` — a health verdict with\n> per-domain summaries, not a span dump. Calls `search_docs {"query": "telemetry"}` →\n> `{"count": 1, "matches": [{"slug": "mcp", "title": "@netscript/mcp", "snippet": "…", "score": 35}]}`,\n> then `get_doc` with the winning slug to read just the section it needs.\n\nWhen telemetry is unreachable, nothing crashes: `get_app_status` and the doctor\'s telemetry checks\nreport a structured `warn`/`fail` status, the list and analytics tools return their ordinary empty\nresults, and `get_run` returns a structured `run_not_found` error the agent can reason about.\n\n## Tool catalog\n\n| Tool | Required input | Bounded result |\n| ----------------------------- | --------------------- | ---------------------------------------------------------------------------- |\n| `get_app_status` | — | Health verdict, counts, per-domain summaries |\n| `list_runs` | — | Recent executions filtered by domain, status, service, time |\n| `get_run` | `id` | One correlated execution with bounded spans and logs |\n| `get_recent_errors` | — | Recent errors grouped by service and domain |\n| `get_last_job_result` | — | The latest matching job outcome |\n| `analyze_service_performance` | `service` | Duration percentiles, throughput, error rate |\n| `analyze_db_bottlenecks` | — | Ranked database and KV operations |\n| `doctor` | — | Telemetry, Aspire, wiring, and plugin checks; suggested fixes on problems |\n| `search_docs` | `query` | Ranked public-document matches with snippets |\n| `list_docs` | — | Public-document summaries |\n| `get_doc` | `slug` | One public document, or one named section of it |\n| `list_commands` | — | Live CLI command descriptors |\n| `execute_command` | `command` | Exit code, duration, and bounded output tail; structured denial when blocked |\n| `record_drift` | `resource`, `summary` | Evidence-gated drift entry appended to project drift log |\n| `list_api_services` | — | Discovered services, live spec status, source outcomes, and operation counts |\n| `list_service_operations` | `service` | Bounded OpenAPI operation rows with honest truncation metadata |\n| `get_operation_schema` | `service`, `operation` | Request, response, and error views plus an unauthenticated curl template |\n\nA top-level input/result field overview for every tool is on the\n[MCP reference](https://rickylabs.github.io/netscript/reference/mcp/); the complete Standard Schema\ncontracts are published as `TOOL_INPUT_SCHEMAS` / `TOOL_OUTPUT_SCHEMAS` and returned by the live\n`tools/list`.\n\n## Record drift\n\n`record_drift` is an evidence-gated mutating tool that records verified architecture or runtime\ndrift into `.netscript/agent/drift.jsonl`.\n\n- **Required evidence**: Requires a fresh successful diagnostic receipt (timestamped within 15\n minutes, `exitStatus: 0`) for the target resource. Receipts are automatically produced when\n calling `doctor`, telemetry tools, or `netscript plugin doctor --resource `.\n- **Target & Scope**: The `resource` argument targets a specific plugin, service, or `\'project\'`.\n Receipts live at `.netscript/agent/diagnostics/.json`.\n- **Mutation behavior**: Appends a single JSON line to `.netscript/agent/drift.jsonl` under the\n project root containing `timestamp`, `resource`, `summary`, optional `details`, and the attached\n evidence receipt.\n- **Failure modes**: If no receipt exists, if the receipt is older than 15 minutes, or if the\n receipt recorded a non-zero exit status, `record_drift` refuses with structured error code\n `diagnostic_evidence_required`.\n- **Dry-run / Preview**: Inspecting receipts or running `doctor` / telemetry tools previews current\n diagnostic state without mutating `drift.jsonl`.\n\n## Embedding as a library\n\nTo run the public stdio composition from your own Deno entrypoint:\n\n```ts\nimport { runMcpStdioServer } from \'@netscript/mcp/cli\';\n\nawait runMcpStdioServer({\n projectRoot: Deno.cwd(),\n // Omit docsRoot to use the package-embedded corpus, or select a filesystem corpus explicitly.\n docsRoot: Deno.env.get(\'NETSCRIPT_DOCS_ROOT\'),\n});\n```\n\n`runMcpStdioServer` owns the newline-delimited stdio transport. It shuts down when the host closes\nstdin or terminates the process; callers do not need to reach into an internal transport API.\n\n## Public surface\n\nThree entrypoints carry the package:\n\n| Entry | What it gives you |\n| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| `.` | Tool contracts and schemas, the tool registry, protocol runner, service endpoint directory ports, and default adapters |\n| `./cli` | The executable composition plus every export from `.`, including the service endpoint directory surface |\n| `./openapi-projection` | Pure OpenAPI operation indexing and schema projections, with no discovery, filesystem, network, or runtime work |\n\nThe projection subpath accepts an already-loaded OpenAPI document. It keeps discovery and I/O at the\ncaller\'s boundary:\n\n````ts\nimport { indexOpenApiOperations } from \'@netscript/mcp/openapi-projection\';\n\nconst index = indexOpenApiOperations(openApiDocument);\n\nEvery tool flow depends on a port interface, so embedders and tests supply their own adapters and\nassert against the published schemas. The always-current symbol list is\n[`deno doc jsr:@netscript/mcp@`](https://jsr.io/@netscript/mcp/doc) (pin `` on the\npre-release line, as above).\n\n### Discover service OpenAPI endpoints\n\nEmbedders can compose the four discovery sources and bounded network probe without importing an\nOpenAPI projection layer:\n\n```ts\nimport { createServiceEndpointDirectory } from \'@netscript/mcp\';\n\nconst endpoints = createServiceEndpointDirectory({\n projectRoot: Deno.cwd(),\n});\n\nconst { entries, sources } = await endpoints.list();\n````\n\nThe effective per-service precedence is `override > aspire-cli > run-manifest > appsettings`. Every\nsource remains visible as `used`, `absent`, or `failed`; a failed Aspire CLI query or a stale\nmanifest is never rendered as healthy absence. The Aspire adapter brackets its machine-readable\n`describe` query with `ps` snapshots: the exact real AppHost path must belong to the project, its\nprocess identity must remain stable across the read, and any executable resource working directory\nmust remain inside that same real project root. For executable services, the adapter prefers the\nallocated target `PORT` from the described resource environment over a fixed proxy URL; this keeps\ndiscovery live when a stale foreign process occupies the requested proxy port. CLI absence, non-zero\nexit, identity drift, and partial JSON are distinct failed source rows. Benign CLI banners,\ntrailers, and casing changes are accepted without weakening those checks. The manifest at\n`.netscript/run/endpoints.json` is eligible only when its real project root and `runId` match the\nsupplied current run. `appHostPath` defaults to `./aspire/apphost.mts`; override it when the active\nAppHost lives elsewhere. Supply `expectedRunId` only when the host owns the current AppHost run\ntoken; without that identity proof, a present run manifest is reported as failed and does not\ncontribute endpoints.\n\nExplicit operator endpoints and exclusions live only in the S5-owned subsection of\n`.netscript/agent-mcp.json`; sibling settings are ignored:\n\n```json\n{\n "introspection": {\n "serviceEndpoints": {\n "orders": "https://orders.example.test"\n },\n "excludeServices": ["internal-admin"]\n }\n}\n```\n\nExclusions are applied before network access. Other rows report `running`, `not_running`,\n`spec_unavailable`, or `identity_mismatch`; parsed OpenAPI is retained as opaque JSON for a later\nconsumer. Probes do not send credentials or follow redirects. A 401/403 explains how to expose only\nthe OpenAPI route anonymously or supply a reachable public spec URL. A running service must return\nJSON containing its selected service name, for example `{ "service": "orders" }`, from its selected\nbase path; this second request prevents a reused port from being mistaken for the intended service.\n\nThe default library composition needs `--allow-read` for carriers and real-path checks,\n`--allow-run` for `aspire ps` plus `aspire describe`, and `--allow-net` for bounded spec/identity\nrequests. Tests and custom hosts can replace every source and the probe through\n`ServiceEndpointDirectoryOptions`.\n\n## Configuration at a glance\n\n- **Telemetry endpoint discovery** (tools and `doctor`): explicit `--endpoint`, then\n `NETSCRIPT_TELEMETRY_ENDPOINT`, then `ASPIRE_DASHBOARD_PORT`, then `http://localhost:18888`.\n- **Docs corpus**: by default the docs tools index the documentation shipped with the installed\n package; set `--docs-root ` (or `NETSCRIPT_DOCS_ROOT`) to serve a project or site corpus\n instead.\n- **Service endpoint discovery** (library surface): `.netscript/agent-mcp.json` override, then the\n Aspire CLI machine-readable query, then an identity-bound run manifest, then\n `aspire/appsettings.json`; lower-priority disagreements remain visible as conflicts.\n- **Command policy**: the shipped default allows the prefixes\n `db init|generate|migrate|seed|status|introspect`, `generate`, `contract`, `service list`,\n `plugin install|list|sync|doctor`, and `ui:add|ui:init|ui:list|ui:update`, and denies `deploy`,\n `init`, `marketplace`, `db reset`, `plugin remove`, and `ui:remove` — deny beats allow, anything\n unmatched is denied. Embedders can pass their own policy.\n\nThe full flag reference, policy table, and composition options are on the docs site.\n\n## Docs\n\n- **MCP reference — the 17-tool field overview, policy, and exports**:\n [rickylabs.github.io/netscript/reference/mcp/](https://rickylabs.github.io/netscript/reference/mcp/)\n- **Agent tooling — install, flags, troubleshooting, CLI × skills × MCP**:\n [rickylabs.github.io/netscript/capabilities/agent-tooling/](https://rickylabs.github.io/netscript/capabilities/agent-tooling/)\n- **API docs on JSR**: [jsr.io/@netscript/mcp/doc](https://jsr.io/@netscript/mcp/doc)\n\n## Compatibility\n\nThe **server** requires Deno 2.9+ (both entrypoints use `Deno.*` APIs); Node.js and Bun are not\nsupported as server runtimes. The **client** side is unconstrained: any MCP-capable host — Claude\nCode, VS Code, and others — only has to spawn the process and speak JSON-RPC over stdio. The\nexecutable needs `--allow-env`, `--allow-net`, `--allow-read`, and `--allow-run`; the `netscript`\nbinary grants these at its edge. The server never returns project source, environment-variable\nvalues, credentials, or secrets.\n\n## License\n\nApache-2.0 — see [LICENSE](https://github.com/rickylabs/netscript/blob/main/LICENSE). Published to\nJSR with cryptographically verified provenance.\n'; diff --git a/packages/mcp/tests/service-endpoint-sources_test.ts b/packages/mcp/tests/service-endpoint-sources_test.ts index ecf68bdd53..5a77726e03 100644 --- a/packages/mcp/tests/service-endpoint-sources_test.ts +++ b/packages/mcp/tests/service-endpoint-sources_test.ts @@ -230,7 +230,7 @@ Deno.test('Aspire CLI absence, non-zero exit, and parse failure are explicit fai Deno.test('Aspire CLI accepts casing drift and bounded trailing output while preserving identity', async () => { const outputs = [ 'Aspire 14\n{"Items":[{"AppHostPath":"/project/aspire/apphost.mts","AppHostPid":"run-7"}]}\ndone', - 'notice\n{"Resources":[{"DisplayName":"users","Endpoints":[{"Url":"http://localhost:41234"}],"Properties":{"WorkDir":"/project/services/users"}}]}\ntelemetry', + 'notice\n{"Resources":[{"DisplayName":"users","Endpoints":[{"Url":"http://localhost:3001"}],"Environment":{"PORT":"41234"},"Properties":{"WorkDir":"/project/services/users"}}]}\ntelemetry', '{"appHosts":[{"appHostPath":"/project/aspire/apphost.mts","appHostPid":"run-7"}]}', ]; const source = new AspireCliEndpointSource({ From 5f2e2fe776db9d270dafbba398ee461a4588ad37 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Tue, 4 Aug 2026 11:37:41 +0200 Subject: [PATCH 4/6] docs(harness): record S7 runtime proof --- .../context-pack.md | 17 ++++++++--------- .../drift.md | 12 ++++++++++++ .../worklog.md | 5 +++-- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md index 48b136688c..38c195886f 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md @@ -6,7 +6,7 @@ | --- | --- | | Run ID | `feat-openapi-mcp-manifest-cli--1133` | | Branch | `feat/openapi-mcp-manifest-cli` | -| Current phase | `gate` | +| Current phase | `implementation evaluation` | | Archetype | `2 - Integration` | | Scope overlays | `none` | @@ -14,7 +14,7 @@ The identity-bound F1(b) adapter and scaffold named gate are implemented. A real proxy collision proved the identity guard and added allocated target-PORT selection. Package/static gates pass; -fresh branch CI must provide the final canonical runtime verdict. +fresh branch CI passed the named live adapter gate and the canonical suite 71/71 with cleanup. ## Completed @@ -28,13 +28,12 @@ fresh branch CI must provide the final canonical runtime verdict. ## In Progress -- Commit/push S3, then watch fresh branch CI and composed evaluation. +- Composed implementation review/sign-off under milestone ruling D6. ## Next Steps -1. Push explicit refspec and watch fresh scaffold-runtime CI. -2. Update acceptance evidence/DoD only if the named gate passes. -3. Hand to composed implementation evaluation. +1. Obtain separate composed implementation review/sign-off. +2. Move to `status:ready-merge` only after the evaluator comment and complete DoD. ## Key Decisions @@ -61,12 +60,12 @@ fresh branch CI must provide the final canonical runtime verdict. | --- | --- | --- | | Static | pass | check + scoped lint/fmt | | Fitness | pass | focused/package tests, quality scan, arch check, JSR dry run | -| Runtime | pending fresh CI | local attempts hit unrelated baseline failures; cleanup clean | -| Consumer | negative case pass / positive pending | named gate proved foreign-port refusal; target-port fix awaits CI | +| Runtime | pass | [CI attempt 2](https://github.com/rickylabs/netscript/actions/runs/30895255613/job/91950747988): canonical suite 71/71 with cleanup | +| Consumer | pass | named live directory gate passed in 4.896s after the earlier foreign-port refusal drove the fix | ## Open Questions -- Will S6 land before the serialized runtime gate? +- Which composed reviewer surface will supply the separate IMPL-EVAL verdict after the automatic OpenHands job was policy-skipped? ## Drift and Debt diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md index 7bee5f302e..a0ad4198ca 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md @@ -43,3 +43,15 @@ - **Severity:** minor - **Action:** defer to fresh branch CI verdict - **Evidence:** `worklog.md`; GitHub scaffold-runtime was green on the prior adapter commit. + +## 2026-08-04 — composed evaluator surface did not auto-start + +- **What:** Draft-to-ready fired repository review workflows, but the OpenHands job was policy-skipped + and no separate review verdict was posted automatically. +- **Source:** Milestone-run evaluator protocol / orchestrator ruling D6 and PR #1206 checks. +- **Expected:** Ready transition plus label surface composes a separate review/evaluation. +- **Actual:** Automated implementation and runtime gates passed; separate reviewer sign-off remains + pending. Cloud OpenHands cannot be forced for this local run under its routing policy. +- **Severity:** minor +- **Action:** leave `status:impl-eval` and request an eligible composed/owner review; never self-certify. +- **Evidence:** PR #1206 timeline and OpenHands Agent check on head `dfcf2b23a`. diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md index 6020d5a108..53b4a45a0e 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md @@ -64,6 +64,7 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | 2026-08-04 | 3 | negative E2E | Named MCP gate rejected fixed proxy port 3001 because it identified as foreign service `products`; no false live row escaped. | | 2026-08-04 | 3 | hardening | Adapter now prefers the real CLI-described executable target `PORT`, retaining declared URL host/protocol normalization. | | 2026-08-04 | 3 | baseline failures | Full one-pass attempts later stopped at existing `behavior.service-health` DB health and `runtime.wait.workers-api` gates before S7. Cleanup passed and leak checks found no survivors. | +| 2026-08-04 | 3 | serialized CI proof | After the baseline holder released the global slot, e2e-cli attempt 2 passed the named MCP gate in 4.896s and the canonical suite completed 71/71 with cleanup. | ## Decisions @@ -109,13 +110,13 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | live current-run query | PASS | direct adapter read | current baseline DB-operation AppHost produced `used` | | `scaffold.runtime` infrastructure | FAIL (baseline) | canonical one-pass command | independent pre-S7 failures: users DB-health aggregate; workers-api timeout | | `behavior.mcp-endpoint-directory` negative case | PASS | canonical one-pass named gate | contested proxy identified as `products`; adapter returned `identity_mismatch` | -| `behavior.mcp-endpoint-directory` target-port case | PENDING_CI | fresh branch run | local static fixture passes; host slot is now held by foreign `wave5-deepseek` AppHost | +| `behavior.mcp-endpoint-directory` target-port case | PASS | [e2e-cli attempt 2](https://github.com/rickylabs/netscript/actions/runs/30895255613/job/91950747988) | named live gate passed in 4.896s; canonical suite 71/71 with cleanup | ### Consumer Gates | Consumer | Result | Evidence | Notes | | --- | --- | --- | --- | -| scaffolded app | NOT_RUN | queued | S6 currently not landed; directory fixture fallback selected. | +| scaffolded app | PASS | [scaffold-runtime CI](https://github.com/rickylabs/netscript/actions/runs/30895255613/job/91950747988) | direct directory-call fallback selected because S6 is not landed; live `users` row resolved through `aspire-cli`. | ## Handoff Notes From 5e11f1894b99cf246c72e8e877f16d347b63f643 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Tue, 4 Aug 2026 11:39:40 +0200 Subject: [PATCH 5/6] test(mcp): exercise list api services live --- .../context-pack.md | 14 +++++++++----- .../worklog.md | 3 ++- .../scaffold/verify-mcp-endpoint-directory.ts | 17 +++++++++++++---- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md index 38c195886f..83ae4ebdcf 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md @@ -14,7 +14,9 @@ The identity-bound F1(b) adapter and scaffold named gate are implemented. A real proxy collision proved the identity guard and added allocated target-PORT selection. Package/static gates pass; -fresh branch CI passed the named live adapter gate and the canonical suite 71/71 with cleanup. +pre-reconcile branch CI passed the named live adapter gate and the canonical suite 71/71 with +cleanup. S6 then landed on main, so the rebased gate now invokes `list_api_services` and awaits a +current-head repeat. ## Completed @@ -28,12 +30,14 @@ fresh branch CI passed the named live adapter gate and the canonical suite 71/71 ## In Progress +- Current-head scaffold runtime after upgrading the live gate to S6's `list_api_services` flow. - Composed implementation review/sign-off under milestone ruling D6. ## Next Steps -1. Obtain separate composed implementation review/sign-off. -2. Move to `status:ready-merge` only after the evaluator comment and complete DoD. +1. Repeat serialized scaffold runtime on the S6-rebased head. +2. Obtain separate composed implementation review/sign-off. +3. Move to `status:ready-merge` only after the evaluator comment and complete DoD. ## Key Decisions @@ -60,8 +64,8 @@ fresh branch CI passed the named live adapter gate and the canonical suite 71/71 | --- | --- | --- | | Static | pass | check + scoped lint/fmt | | Fitness | pass | focused/package tests, quality scan, arch check, JSR dry run | -| Runtime | pass | [CI attempt 2](https://github.com/rickylabs/netscript/actions/runs/30895255613/job/91950747988): canonical suite 71/71 with cleanup | -| Consumer | pass | named live directory gate passed in 4.896s after the earlier foreign-port refusal drove the fix | +| Runtime | pending current head | [pre-reconcile CI attempt 2](https://github.com/rickylabs/netscript/actions/runs/30895255613/job/91950747988): canonical suite 71/71 with cleanup | +| Consumer | pending current head | directory fallback passed live; rebased gate now invokes S6 `list_api_services` flow | ## Open Questions diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md index 53b4a45a0e..2637ac8483 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md @@ -65,6 +65,7 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | 2026-08-04 | 3 | hardening | Adapter now prefers the real CLI-described executable target `PORT`, retaining declared URL host/protocol normalization. | | 2026-08-04 | 3 | baseline failures | Full one-pass attempts later stopped at existing `behavior.service-health` DB health and `runtime.wait.workers-api` gates before S7. Cleanup passed and leak checks found no survivors. | | 2026-08-04 | 3 | serialized CI proof | After the baseline holder released the global slot, e2e-cli attempt 2 passed the named MCP gate in 4.896s and the canonical suite completed 71/71 with cleanup. | +| 2026-08-04 | 3 | S6 reconcile | Main gained S6/#1204 before handoff, so the named gate was upgraded from the permitted directory fallback to `createListApiServicesFlow`; current-head CI must repeat the live proof. | ## Decisions @@ -116,7 +117,7 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | Consumer | Result | Evidence | Notes | | --- | --- | --- | --- | -| scaffolded app | PASS | [scaffold-runtime CI](https://github.com/rickylabs/netscript/actions/runs/30895255613/job/91950747988) | direct directory-call fallback selected because S6 is not landed; live `users` row resolved through `aspire-cli`. | +| scaffolded app | PENDING_CURRENT_HEAD | [pre-S6-reconcile scaffold-runtime CI](https://github.com/rickylabs/netscript/actions/runs/30895255613/job/91950747988) | direct directory fallback passed live; S6 has since landed and the gate now calls `list_api_services`, requiring one current-head rerun. | ## Handoff Notes diff --git a/packages/cli/e2e/src/application/gates/scaffold/verify-mcp-endpoint-directory.ts b/packages/cli/e2e/src/application/gates/scaffold/verify-mcp-endpoint-directory.ts index 55c1662151..a6165e5fa4 100644 --- a/packages/cli/e2e/src/application/gates/scaffold/verify-mcp-endpoint-directory.ts +++ b/packages/cli/e2e/src/application/gates/scaffold/verify-mcp-endpoint-directory.ts @@ -1,20 +1,29 @@ -import { createServiceEndpointDirectory } from '../../../../../../mcp/mod.ts'; +import { + createListApiServicesFlow, + createServiceEndpointDirectory, + type ListApiServicesResult, +} from '../../../../../../mcp/mod.ts'; const [projectRoot, appHostPath] = Deno.args; if (!projectRoot || !appHostPath) { throw new Error('project root and AppHost path are required'); } -const result = await createServiceEndpointDirectory({ projectRoot, appHostPath }).list(); +const directory = createServiceEndpointDirectory({ projectRoot, appHostPath }); +const receipt = await createListApiServicesFlow(directory)({}); +if (!receipt.ok) { + throw new Error(`list_api_services failed: ${JSON.stringify(receipt.error)}`); +} +const result = receipt.value as ListApiServicesResult; const aspire = result.sources.find((source) => source.source === 'aspire-cli'); if (aspire?.outcome !== 'used') { throw new Error(`aspire-cli source was not used: ${JSON.stringify(aspire)}`); } -const users = result.entries.find((entry) => entry.name === 'users'); +const users = result.services.find((entry) => entry.name === 'users'); if (users?.status !== 'running' || users.source !== 'aspire-cli') { throw new Error( `users endpoint did not resolve live through aspire-cli: ${JSON.stringify(users)}`, ); } -console.log(`aspire-cli resolved users at ${users.baseUrl}`); +console.log(`list_api_services resolved users through aspire-cli at ${users.baseUrl}`); From 14db53ace3f3ae2f5bdec7a2a4555e04129d38c6 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Tue, 4 Aug 2026 13:28:44 +0200 Subject: [PATCH 6/6] docs(harness): prove live list api services discovery --- .../context-pack.md | 16 +++++++-------- .../drift.md | 13 ++++++++++++ .../leak-report.md | 20 +++++++++++++++++-- .../worklog.md | 6 +++++- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md index 83ae4ebdcf..fb906766cf 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/context-pack.md @@ -15,8 +15,8 @@ The identity-bound F1(b) adapter and scaffold named gate are implemented. A real proxy collision proved the identity guard and added allocated target-PORT selection. Package/static gates pass; pre-reconcile branch CI passed the named live adapter gate and the canonical suite 71/71 with -cleanup. S6 then landed on main, so the rebased gate now invokes `list_api_services` and awaits a -current-head repeat. +cleanup. After S6 and #1211 landed, a fresh randomized-port scaffold passed the public +`list_api_services` capture against its exact AppHost while a foreign AppHost remained live. ## Completed @@ -27,17 +27,17 @@ current-head repeat. project resources, and AppHost restart races. - Package check/tests, scoped lint/fmt, quality/architecture, docs, and publish dry-run gates pass. - Canonical S7 negative case rejected a foreign service on the fixed proxy port. +- Box-43 positive case correlated `aspire describe` target `PORT=45253` to the tool's live + `users` base URL `http://127.0.0.1:45253`, source `aspire-cli`, with 3 operations. ## In Progress -- Current-head scaffold runtime after upgrading the live gate to S6's `list_api_services` flow. - Composed implementation review/sign-off under milestone ruling D6. ## Next Steps -1. Repeat serialized scaffold runtime on the S6-rebased head. -2. Obtain separate composed implementation review/sign-off. -3. Move to `status:ready-merge` only after the evaluator comment and complete DoD. +1. Obtain separate composed implementation review/sign-off. +2. Move to `status:ready-merge` only after the evaluator comment and complete DoD. ## Key Decisions @@ -64,8 +64,8 @@ current-head repeat. | --- | --- | --- | | Static | pass | check + scoped lint/fmt | | Fitness | pass | focused/package tests, quality scan, arch check, JSR dry run | -| Runtime | pending current head | [pre-reconcile CI attempt 2](https://github.com/rickylabs/netscript/actions/runs/30895255613/job/91950747988): canonical suite 71/71 with cleanup | -| Consumer | pending current head | directory fallback passed live; rebased gate now invokes S6 `list_api_services` flow | +| Runtime | pass | pre-reconcile suite 71/71 plus D15 exact-path adapter capture on #1211-rebased head | +| Consumer | pass | public MCP `list_api_services` returned live `users` at the describe-correlated target port | ## Open Questions diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md index a0ad4198ca..48b04abb85 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/drift.md @@ -55,3 +55,16 @@ - **Severity:** minor - **Action:** leave `status:impl-eval` and request an eligible composed/owner review; never self-certify. - **Evidence:** PR #1206 timeline and OpenHands Agent check on head `dfcf2b23a`. + +## 2026-08-04 — D15 waives serialization for adapter-level capture only + +- **What:** The box-43 adapter capture ran alongside foreign AppHosts; the full `scaffold.runtime` + suite remains serialized. +- **Source:** Written orchestrator ruling D15. +- **Expected:** Earlier expensive-gate law blocked any local AppHost while another was live. +- **Actual:** #1211 randomizes listener defaults, while S7 exact-path project/run binding and + service identity checks discriminate the selected AppHost. Coexistence is the strongest identity + proof for this adapter-level capture. +- **Severity:** minor +- **Action:** accept for box 43 only; leave foreign resources untouched and stop only the exact S7 AppHost. +- **Evidence:** Fresh `s7-box43-live` describe/tool correlation recorded in `worklog.md` and PR #1206. diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/leak-report.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/leak-report.md index d3a4a46a48..88b08a5633 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/leak-report.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/leak-report.md @@ -1,7 +1,23 @@ # Run resource leak report -Generated: 2026-08-04T08:39:53.743Z +Generated: 2026-08-04T11:21:49.172Z Worktree: `/home/codex/repos/ns005-s7` Aspire probe: ok Docker probe: ok -No surviving Aspire resources found. + +## container: postgres-d76868c4 (bdd5011afacb1dee73ccc19e6e3c59661845edf1410c8853783c568754f80a29) + +- Ownership: `foreign` +- Apparent owner: `/home/codex/repos/ns005-sagas` +- Age: 656794 ms +- Stale: false +- User command: `docker rm -f 'bdd5011afacb1dee73ccc19e6e3c59661845edf1410c8853783c568754f80a29'` + +## container: postgres-89449635 (97b90646098858f6cfe163b470fb9d57ff7033d5661f6fe2390c9300ff1ebaec) + +- Ownership: `foreign` +- Apparent owner: `/home/codex/repos/wave5-deepseek` +- Age: 4927778 ms +- Stale: false +- User command: `docker rm -f '97b90646098858f6cfe163b470fb9d57ff7033d5661f6fe2390c9300ff1ebaec'` + diff --git a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md index 2637ac8483..390048b475 100644 --- a/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md +++ b/.llm/runs/feat-openapi-mcp-manifest-cli--1133/worklog.md @@ -66,6 +66,8 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | 2026-08-04 | 3 | baseline failures | Full one-pass attempts later stopped at existing `behavior.service-health` DB health and `runtime.wait.workers-api` gates before S7. Cleanup passed and leak checks found no survivors. | | 2026-08-04 | 3 | serialized CI proof | After the baseline holder released the global slot, e2e-cli attempt 2 passed the named MCP gate in 4.896s and the canonical suite completed 71/71 with cleanup. | | 2026-08-04 | 3 | S6 reconcile | Main gained S6/#1204 before handoff, so the named gate was upgraded from the permitted directory fallback to `createListApiServicesFlow`; current-head CI must repeat the live proof. | +| 2026-08-04 | 3 | D15 live capture | On the #1211-rebased head, a fresh `s7-box43-live` scaffold ran beside foreign AppHosts. Exact-path `aspire describe` exposed `users` proxy `45889` and allocated target `PORT=45253`; the public MCP `list_api_services` tool returned `users` running from `aspire-cli` at `http://127.0.0.1:45253` with 3 operations. | +| 2026-08-04 | 3 | cleanup | Stopped only the exact S7 AppHost. A foreign sagas AppHost remained live before, during, and after the capture; no foreign AppHost/container was touched. | ## Decisions @@ -74,6 +76,7 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | Qualified F1(b), no template emission | P1 locked verdict | #1123 §F1 / P1 verdict / owner brief | | CLI run binding is exact AppHost path + stable PID | Facts exposed by real Aspire 13.4.6 `ps` | research D1-D3 | | PLAN-EVAL is composed/not-local | Milestone evaluator rule | owner brief / milestone-run D6 | +| Adapter capture may coexist with foreign AppHosts | Exact-path/run/service identity is the subject under test; full suite remains serialized | orchestrator ruling D15 | ## Drift @@ -112,12 +115,13 @@ fixture that proves both accepted drift and rejection of an adjacent torn/ambigu | `scaffold.runtime` infrastructure | FAIL (baseline) | canonical one-pass command | independent pre-S7 failures: users DB-health aggregate; workers-api timeout | | `behavior.mcp-endpoint-directory` negative case | PASS | canonical one-pass named gate | contested proxy identified as `products`; adapter returned `identity_mismatch` | | `behavior.mcp-endpoint-directory` target-port case | PASS | [e2e-cli attempt 2](https://github.com/rickylabs/netscript/actions/runs/30895255613/job/91950747988) | named live gate passed in 4.896s; canonical suite 71/71 with cleanup | +| box-43 `list_api_services` positive capture | PASS | [PR #1206 evidence](https://github.com/rickylabs/netscript/pull/1206#issuecomment-5178346127) | fresh randomized-port scaffold; describe target `45253` matched tool base URL; foreign AppHost stayed live | ### Consumer Gates | Consumer | Result | Evidence | Notes | | --- | --- | --- | --- | -| scaffolded app | PENDING_CURRENT_HEAD | [pre-S6-reconcile scaffold-runtime CI](https://github.com/rickylabs/netscript/actions/runs/30895255613/job/91950747988) | direct directory fallback passed live; S6 has since landed and the gate now calls `list_api_services`, requiring one current-head rerun. | +| scaffolded app | PASS | [PR #1206 box-43 evidence](https://github.com/rickylabs/netscript/pull/1206#issuecomment-5178346127) | public `tools/call` → `list_api_services` selected this AppHost's randomized allocated target port and returned the live `users` service from `aspire-cli`. | ## Handoff Notes