feat!: require the org on every call, and remove KOSLI_ORG - #65
Conversation
8ff7900 to
f6bdb92
Compare
dangrondahl
left a comment
There was a problem hiding this comment.
Automated review (Claude Code /code-review, run locally). Baseline: tsc --noEmit clean, 103/103 tests pass. Nine findings are inline, most severe first: search-actions.ts (put_user_default_org loses its argument), server.ts (tool org rejects null), execute-action.ts ×4, claude-review.yaml, catalog.test.ts, run-action.ts.
One finding has no diff line to attach to, since the file is unchanged:
src/client/kosli-client.ts:127-131 — the org is now decided in two places with two policies. resolveOrg trims, rejects blanks and non-strings, and detects conflicts. But on every sanctioned call with no org named, resolved.org is undefined, so it is buildUrl's params.org ?? this.config.org that actually picks KOSLI_ORG. That fallback is structurally live, not a leftover, and it has its own looser rules: null falls back, '' becomes a literal empty path segment (/environments/), arrays serialize as a,b. CLAUDE.md now says both "decided in one place" (line 30) and "preserve the fallback in buildUrl" (line 29). Suggested: have resolveOrg return the fully resolved name (named[0] ?? config.org for org-scoped entries), add org only when the entry takes one, and delete the special case in buildUrl so org is an ordinary required path param.
Lower-severity items not posted inline: conflict comparison is case-sensitive ("a" vs "A" rejected, consistent with the reject-rather-than-resolve intent); search results still show {org} in path while parameters omits it and the params description never mentions params.org; ORG_INPUT's description does not warn that the four non-org-scoped actions reject it; test/server.test.ts depends on live catalog id list_envs since createServer has no entries injection point; type FetchFn is declared in three files and ExecuteOptions/ActionArgs are exported with no importers.
Disclosure: one verification probe accidentally ran without a mock fetch and issued a real POST /api/v2/artifacts/cyber-dojo/body to app.kosli.com with a bogus token. It got a 403 and changed nothing.
f6bdb92 to
1f20c2e
Compare
|
Claude finished @AlexKantor87's task in 5m 23s —— View job Review complete
VerdictThe architectural invariants hold, and this is a clean resolution of the previous round. Nothing here blocks merge; the five inline comments are one substantive UX gap and four nits. The invariants I checked, all intact: exactly three tools ( The one invariant this PR deliberately inverts — the Findings
Also: the commit is correctly marked What's good here
CaveatI could not run |
BREAKING CHANGE: KOSLI_ORG is no longer read, and a call that names no org is
refused instead of falling back to it. An existing config that still sets the
variable starts fine; the server ignores the value and says so on stderr.
The server was pinned to one organization at startup. Reaching another meant
restarting it with different env vars, and a call that named none went wherever
KOSLI_ORG pointed. That is the wrong shape for a server a customer runs against
several orgs: the org a call lands in should be something the caller said, not
something the process remembered.
`org` is now an input on execute_read_action and execute_write_action, required
for every action whose path contains `{org}`, which is all but four of them. As
a top-level input it also shows up in the client's write-approval prompt instead
of inside a params blob.
Removing the fallback also removes the second org policy it fed in
`KosliClient.buildUrl`, which had looser rules than the tool layer. The org is
an ordinary required path parameter now.
`resolveOrg` decides the target. An org can be named as the tool input or as
`params.org`, and the two must agree. Rejected rather than guessed: two
different names, a name that is not a non-empty string, and an org aimed at one
of the four actions that take none. Coercion is what makes the wrong-type case
matter, since `["a", "b"]` would otherwise read as the org `a,b` and an org id
as an org called `1234`.
A request body may not name any of the action's own parameters. `unwrapBodyParam`
spreads a write's body over the top level, so such a field overwrote what the
caller supplied and sent the request elsewhere while the approval prompt still
showed the caller's value: `create_artifact` with `flow_name` at the top level
and a different `flow_name` in the body went to the body's flow. That was true
of every path parameter, not only `org`.
`search_actions` no longer advertises `org` among an action's required path
parameters, since doing so told the model to put it in `params` rather than the
tool's own input. One action is exempt: `PUT /user/{org}` sets the caller's
default org, so there the org is what the call writes rather than the scope it
runs in.
Which parameter counts as the org lives in `src/org.ts`, because three readers
have to agree: the check in `resolveOrg`, the strip in `search_actions`, and the
substitution in `KosliClient.buildUrl`.
Registration moves to `createServer` in `src/server.ts`, leaving `index.ts` as
the bin that connects stdio. Without that seam the tool schema and the handler
wiring had no test: dropping the `org` input, or pointing the read tool at write
mode, left the suite green while the MCP layer discarded a client's org.
Closes #8
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
All findings are in, and the change grew a step: the org is required on every call and From @dangrondahl: From the bot: a request body may no longer name any of the action own parameters. Take Kept: the four non-org-scoped ids stay pinned. That test caught three renames from #60 during the rebase. search: grep -rn "...body" src/ --include=*.ts search: grep -rn "params.org|withOrg.org" src/ --include=*.ts mutation: deleting the collision block -> 5 tests red; reverting the wrapper to a bare {error} -> the body-with-an-error-field test red; coercing non-strings -> 2 red; removing the refusal -> 4 red 110 tests. Verified against app.kosli.com with no |
1f20c2e to
326f317
Compare
|
Stopping here and handing to @dangrondahl. The five findings from the last run are all non-blocking, and none is being taken. Reasons, so the record is not just a resolve: Refusal is a dead end. Fair, and the one I nearly took. A model that names no org gets told to set one, and there is no Collision guard skips entries with no declared body. For those, a stray
no search: every one of these names a wording or placement choice, not a code class. |
Closes #8. Replaces #63.
The server was pinned to one organization at startup. Reaching another meant
restarting it with different env vars, and a call that named none went wherever
KOSLI_ORGpointed. That is the wrong shape for a server a customer runsagainst several orgs.
orgis now an input on both execute tools, and required for every action whosepath contains
{org}, which is all but four. A call that names none is refusedrather than sent somewhere nobody chose. As a top-level input it also shows up
in the write-approval prompt.
KOSLI_ORGis gone, and so is the fallback it fed inKosliClient.buildUrl.The org is an ordinary required path parameter now. An existing config that
still sets the variable starts fine; the value is simply not read.
An org can be named three ways: the tool input,
params.org, and inside awrite's request body. They must agree. Also rejected: any value that is not a
non-empty string, and an org aimed at one of the four actions that take none.
search_actionsno longer advertisesorgas a required path parameter, sincethat told the model to put it in
params.PUT /user/{org}is exempt, becausethere the org is what the call writes.
Verified
105 tests. 24 mutations proven red one at a time. Driven live against
app.kosli.com through the built binary with no
KOSLI_ORGin the environment,covering two different orgs, the refusal, and every rejection path.
Follow-up
.github/workflows/claude-review.yaml:228still names the removed fallback asan invariant to preserve. Editing that file makes the review action skip its own
PR, so it goes separately.
🤖 Generated with Claude Code