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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2024-05-24 - [Overly Permissive CORS Default]
**Vulnerability:** Fastify API lacked any CORS configuration or security headers, meaning if deployed, it might have been open or prone to basic attacks without Helmet protection.
**Learning:** Default fastify instances do not ship with basic security protections like CORS origin validation or essential HTTP headers.
**Prevention:** Always enforce strict `allowedOrigins` via environment variables (like `VSPEC_ALLOWED_ORIGINS`) and include `@fastify/helmet` as a foundational security measure for any exposed API.
2 changes: 2 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"typecheck": "cd ../.. && tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@fastify/cors": "~8.5.0",
"@fastify/helmet": "~11.1.1",
"@vooster/contracts": "workspace:*"
}
}
12 changes: 12 additions & 0 deletions apps/api/src/http/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Fastify, { type FastifyInstance } from "fastify";
import fastifyCors from "@fastify/cors";
import fastifyHelmet from "@fastify/helmet";
import { healthResponseSchema } from "@vooster/contracts";
import { createMemoryApiKeyStore } from "../infrastructure/memory-api-key-store.js";
import { createMemoryActorStore } from "../infrastructure/memory-actor-store.js";
Expand Down Expand Up @@ -88,6 +90,16 @@ export async function createServer(options: ServerOptions): Promise<FastifyInsta
const userStore = serverOptions.signupStore ?? createMemoryUserStore();
const workspaceStore = serverOptions.signupStore ?? createMemoryWorkspaceStore();
const workSessionStore = serverOptions.signupStore ?? createMemoryWorkSessionStore();
const allowedOrigins = serverOptions.allowedOrigins ?? [
"http://localhost:3000",
"http://127.0.0.1:3000"
];
await app.register(fastifyHelmet);
await app.register(fastifyCors, {
origin: allowedOrigins,
credentials: true
});

if (serverOptions.authStub) {
await seedStubZeroWorkspaceUser(userStore);
}
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/http/signup-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type GithubOAuthConfig = {
};

export type ServerOptions = {
allowedOrigins?: string[];
authStub: boolean;
githubOAuth?: GithubOAuthConfig;
signupStore?: SignupStore;
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ export async function startApi(runtime: Partial<ApiRuntime> = {}) {
const authStub = env.VSPEC_AUTH_STUB === "1";
const forceMemoryStore = env.VSPEC_FORCE_MEMORY_STORE === "1";
const port = portFrom(env.PORT);
const allowedOrigins =
env.VSPEC_ALLOWED_ORIGINS !== undefined
? env.VSPEC_ALLOWED_ORIGINS.split(",")
: undefined;

const app = await (runtime.createServer ?? createServer)({
allowedOrigins,
authStub,
githubOAuth: githubOAuthFromEnv(authStub, env),
signupStore:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ got connected.
- `json` -- the structured `VerifyResult` with a top-level `status`.
- `agent` -- the agent envelope whose `data.status` and `data.drift` an agent
can branch on.
The exit code follows the verdict exactly as `vspec verify` already does
(`0` pass, `7` unlinked steps, `1` otherwise).
The exit code follows the verdict exactly as `vspec verify` already does
(`0` pass, `7` unlinked steps, `1` otherwise).
3. No duplicated verdict logic: there is exactly **one** `runVerify` definition
under `apps/cli/src`, and **every** `apps/cli/src` file that references it
(other than the file that defines it) imports the shared producer rather than
Expand All @@ -76,7 +76,7 @@ The set of accepted formats is the whitelist in `verifyFormat`
(`apps/cli/src/commands/verify.ts`): `human`, `json`, `agent`. The set of
`apps/cli/src` files that reference the verdict producer is enumerated from
source with `grep -rln 'runVerify' apps/cli/src`; exactly one of them may
*define* `runVerify`, and the gate loops over the rest to confirm each imports
_define_ `runVerify`, and the gate loops over the rest to confirm each imports
the shared definition rather than re-declaring it.

## Verification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ So the human view silently diverges from the agent / json / markdown views.
- its **extension point** (e.g. `2a`),
- its **condition** (e.g. `Title is empty`), and
- its **outcome** when present (one of `FAILURE` / `PARTIAL` / `SUCCESS`).
Recovery **steps** continue to render, exactly as before, when present. No
extension is silently dropped.
Recovery **steps** continue to render, exactly as before, when present. No
extension is silently dropped.
3. Format parity holds: for **every** output format `usecase show` accepts
(`human`, `json`, `agent`), the extension data is present in the rendered
output. `json` and `agent` already serialize the raw scenarios; `human` now
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ no pass/fail summary, no findings. The agent could not rely on `verify` and fell
back to reading `usecase show` ("The spec reads correctly") to validate
correctness by eye, making the verify step dead weight before push.

Goal 43 closed the *routing* half of this symptom (verify no longer dead-ends on
a banner). DF-001 is the *substance* half: even when verify runs, it only checks
Goal 43 closed the _routing_ half of this symptom (verify no longer dead-ends on
a banner). DF-001 is the _substance_ half: even when verify runs, it only checks
link drift, so a structurally broken spec (missing actor, empty scenario,
dangling extension point, missing Cockburn field) still verifies clean. This
goal makes verify actually inspect the spec and emit a per-check verdict an agent
Expand Down Expand Up @@ -96,7 +96,7 @@ stays single-source, and `usecase.ts` still routes the `verify` action into it).

The set of `apps/cli/src` files that reference the spec-check producer is
enumerated from source with `grep -rln 'runSpecChecks' apps/cli/src`; exactly one
of them may *define* `runSpecChecks` (enumerated with
of them may _define_ `runSpecChecks` (enumerated with
`grep -rln 'function runSpecChecks' apps/cli/src`), and the gate loops over the
rest to confirm each imports the shared definition rather than re-declaring it.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Two things must become true:
1. The guide must stop shipping self-contradicting create examples: every
`vspec usecase create` example command the guide emits must carry a title the
validator accepts, and none of those create examples may pass `--force`. A
self-teaching example must demonstrate a title that *works*, not one that has
self-teaching example must demonstrate a title that _works_, not one that has
to be forced.
2. When `usecase create` genuinely rejects a title, the `TITLE_NOT_VERB_PHRASE`
response must teach the fix end to end: it must surface at least one concrete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,3 @@ do next (`docs/06-api-contract.md`).
`unlinked_steps`, `failing_tests`) beyond folding structural gaps into the
shared `status` / `exit_code` rollup.
- No prior goal gate may be weakened to pass this goal.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ When an agent runs `vspec usecase verify <id>` (or the equivalent `vspec verify
clean, the command must tell the agent what to do about it -- not just that
something failed. Goal 43 already made `verify` route into `runVerify` and emit a
stable `status` in every format; that closed the "opaque banner" symptom for the
*pass* case. But the finding behind this goal is the *fail* case: when verify
_pass_ case. But the finding behind this goal is the _fail_ case: when verify
reports drift the agent still has to reverse-engineer the remedy by hand. A
verification command whose failure output cannot be acted on programmatically
forced the dogfood agent to abandon `verify` and validate by eye.
Expand Down Expand Up @@ -93,7 +93,7 @@ stays single-source, and `usecase.ts` still routes the `verify` action into it).

The set of `apps/cli/src` files that reference the remediation producer is
enumerated from source with `grep -rln 'suggestVerifyActions' apps/cli/src`;
exactly one of them may *define* `suggestVerifyActions` (enumerated with
exactly one of them may _define_ `suggestVerifyActions` (enumerated with
`grep -rln 'function suggestVerifyActions' apps/cli/src`), and the gate loops over
the rest to confirm each imports the shared definition rather than re-declaring
it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ and the use cases are still at `DRAFT`.
## Source finding

A task asked the agent to act on what `verify` reported. For POCKET-001..005,
`verify` flagged *every* step as `unlinked` and suggested adding `implements`
`verify` flagged _every_ step as `unlinked` and suggested adding `implements`
refs β€” while `verify` itself reported `Tests not run`, the repo contained no
implementation code, and all five use cases were still at `DRAFT`. Meanwhile
every real spec gate was green (`actors_registered`, `scenario_completeness`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ This resolves
purpose remains unclear"). The agent could not determine what verify checks or
whether its spec passed.

Goals 43/54/55 made the *verdict* rich and actionable but operated on
Goals 43/54/55 made the _verdict_ rich and actionable but operated on
`runUsecase`/`runVerify` internals; they were verified through direct calls and a
frozen route snapshot, neither of which enumerates the `runUsecase` action set
against the live route table. So the missing `usecase verify` route slipped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A closed whitelist of English verbs is a correctness liability: it silently
degrades valid specs, it is impossible to keep complete, and it cuts against the
language-aware / Korean-first design principle. The dogfood agent could not
recover from the rejection on its own β€” the message ("Use case title should be a
verb phrase") never named *which* word it disliked, so the agent built the wrong
verb phrase") never named _which_ word it disliked, so the agent built the wrong
mental model ("the validator only accepts a leading subject like `User`") and
fell back to `--force`.

Expand All @@ -25,7 +25,7 @@ Two things must become true:
the fix is a more permissive heuristic or a meaningfully broadened verb set,
the dogfood-discovered title and a representative spread of common finite
verbs the old closed set omitted must all be accepted.
2. When a title *is* genuinely rejected, the `TITLE_NOT_VERB_PHRASE` response
2. When a title _is_ genuinely rejected, the `TITLE_NOT_VERB_PHRASE` response
must name the offending word so an agent can reason about the fix rather than
guess. `--force` stays a real escape hatch; it must not be the only way past a
legitimate verb phrase.
Expand Down Expand Up @@ -53,7 +53,7 @@ and 52 β€” it does not weaken either prior gate.
`titleLooksLikeVerbPhrase`.** The corpus is a source-of-truth fixture,
`apps/api/tests/fixtures/legitimate-verb-phrase-titles.txt` (one title per
line; `#` comment and blank lines ignored). This is a universal claim: the
gate enumerates every non-comment line from that file and loops the *real*
gate enumerates every non-comment line from that file and loops the _real_
validator over each one β€” no single-case cheat. The corpus must encode the
dogfood regression anchor (`Partner accepts a shared-budget invitation`) and a
representative spread of common finite verbs the prior closed set omitted, so
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ gate. It extends the same envelope discipline to the `actor` read/by-id commands
cannot resolve the supplied actor, the emitted envelope names the lookup key
the agent passed and its `suggested_next_actions` points at `vspec actor list`
(so the agent can retry with the listed id). Behaviour β€” in both `--format
agent` and the default human output β€” is locked by unit tests in
agent` and the default human output β€” is locked by unit tests in
`apps/cli/tests/unit/actor-command.test.ts`.
3. **The actor read/write happy paths stay green.** `actor list`/`show`/`edit`/
`archive` success output is unchanged, and the success cases still emit no
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ guide's examples used the literal placeholder `<main-scenario-id>`. The dogfood
agent could not template the command, so it fell back to grepping
`vspec usecase show TODO-001 --format=agent | python3 … data['scenarios'][].id`
β€” running `vspec usecase show` six times to dig the id back out of JSON. Its own
narration: *"Step adds need a scenario id, not the use-case key. … neither
narration: _"Step adds need a scenario id, not the use-case key. … neither
usecase create nor scenario add's tail output surfaced that id clearly. … echoing
the new scenario id prominently on scenario add would close that gap."*
the new scenario id prominently on scenario add would close that gap."_

The root cause is the API success path. `sendCreateScenarioResult`'s `CREATED`
branch in `apps/api/src/http/scenario-results.ts` sends `scenario`/`revision`/
Expand Down Expand Up @@ -67,7 +67,7 @@ lists in `scenario-results.ts`). It extends that discipline to the scenario

1. **Every scenario type the contracts enum declares yields a real-id step-add
next action on create.** The source of truth is `scenarioTypeSchema =
z.enum([...])` in `packages/contracts/src/scenario.ts`. This is a universal
z.enum([...])` in `packages/contracts/src/scenario.ts`. This is a universal
claim: the gate enumerates every enum member from that line and, for each,
drives the **real** `sendCreateScenarioResult` (from
`apps/api/src/http/scenario-results.ts`) over a `CREATED` result whose
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
The dogfood agent authored `POCKET-001` **entirely through CLI write commands**
(`vspec usecase create`, `vspec scenario add`, `vspec step add` β€” narration
commands 10, 12, 14-20). It made **zero** direct edits to any synced spec file
(the digest's *"Direct edits to synced spec state"* section is empty). Yet
(the digest's _"Direct edits to synced spec state"_ section is empty). Yet
command 24, the very first `vspec push`, reported a **conflict** on the local
markdown file:

> *"Push reports a conflict on the local markdown file ... The server already
> _"Push reports a conflict on the local markdown file ... The server already
> holds my latest revision (`113902c9`); the local markdown cache is stale. Let
> me pull to reconcile."*
> me pull to reconcile."_

The agent then had to run command 29 `vspec pull --format=agent` followed by
command 30 `vspec push` just to reconcile a file it had never hand-edited. A
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ teaching:

1. **No key supplied.** `runVerify` in `apps/cli/src/commands/verify.ts` resolves
its argument through `verifyFlagsFrom` β†’ `requiredArgument(usecaseId,
"usecase-id")`, which throws a bare `Error("Missing usecase-id.")`. That error
"usecase-id")`, which throws a bare `Error("Missing usecase-id.")`. That error
has no stable `code` and no `suggested_next_actions`, and it propagates to
oclif's top-level handler, which stringifies it to stderr. (Depending on how
the empty argument is threaded, the request can also reach the API and come
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The fix has two halves:
2. **Give entity NOT_FOUND responses a recovery that teaches the real fix.** An
entity-lookup 404 should point the agent at re-reading the use case to get the
current ids -- `{ command: "vspec usecase show <KEY>", reason: "Re-read the
use case to get the current scenario/step ids." }` -- instead of a signup
use case to get the current scenario/step ids." }` -- instead of a signup
command. The exact wording is the implementer's call, but the recovery must
name `vspec usecase show` (the command that surfaces current ids) and must not
mention `vspec login` / "Restart signup".
Expand Down Expand Up @@ -70,7 +70,7 @@ removes a wrong default and gives entity NOT_FOUND responses a self-teaching one
2. **The shared `problem()` helper no longer ships a signup-flavored default.**
This is the negative universal invariant: because the corpus loop can only
exercise the senders it lists, a single grep over the helper guarantees no
*other* (present or future) `problem()` caller silently inherits the signup
_other_ (present or future) `problem()` caller silently inherits the signup
recovery. The gate fails if the `problem()` default `suggestedNextActions`
parameter still hardcodes `Restart signup`.
3. **Entity NOT_FOUND responses are self-teaching and the genuinely auth-related
Expand Down
Loading
Loading