feat(sdk): publish-ready @sanction/sdk@0.6.0 — escalate loop + npm workflow#169
Conversation
…rkflow Un-private the TypeScript SDK under FSL-1.1-MIT, close the escalate→grant client loop (getAuthorization + sanctionTool grantId), and add a manual publish-sdk workflow mirroring sanction-mcp. Python adapters stay Next. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe SDK adds authorization polling and grant-based retry support, updates package metadata and licensing for version 0.6.0, and introduces a manual npm publishing workflow. Documentation, roadmap, changelog, and distribution guidance are updated accordingly. ChangesSDK publish and authorization flow
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Tool as sanctionTool
participant Client as SanctionClient
participant API as Authorization API
Tool->>Client: authorizeTool with optional grantId
Client->>API: POST /authorize/tool
API-->>Client: escalated requestId
Client->>API: GET /authorize/{requestId}
API-->>Client: approved status and grantId
Tool->>Client: retry with grantId
Client->>API: authorizeTool with grant_id
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/publish-sdk.yml:
- Around line 37-42: Add a workflow-level concurrency configuration to serialize
manual publishes for this package, using a stable group identifier such as the
workflow name and package/ref, and configure appropriate cancellation behavior
so concurrent runs cannot both reach the npm publish logic.
In `@sdk/README.md`:
- Around line 42-46: The README example’s “poll until approved” wording is
inconsistent with its single getAuthorization call. Update the example around
sanction.getAuthorization to either implement bounded polling with backoff until
approval or timeout, or clearly describe it as one status check and instruct
callers to schedule subsequent checks before retrying with status.grantId.
In `@sdk/src/adapters.test.ts`:
- Around line 116-117: The test tool’s execute implementation does not accept
the argument passed by t.execute({}). Update the sanctionTool call in the
relevant test to define execute with a compatible input parameter while
preserving its "ok" result, so the inferred tool type matches the invocation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 45f9db87-c219-40c3-9836-b4a90b2e75a2
📒 Files selected for processing (15)
.github/workflows/publish-sdk.yml.gitignoreREADME.mddocs/BACKLOG.mddocs/COMMERCIAL-LICENSE.mddocs/FRAMEWORK-ADAPTERS.mdlib/changelog.tslib/roadmap.tssdk/LICENSEsdk/README.mdsdk/package.jsonsdk/src/adapters.test.tssdk/src/adapters.tssdk/src/client.tssdk/src/types.ts
| VERSION=$(node -p "require('./package.json').version") | ||
| if npm view "@sanction/sdk@$VERSION" version >/dev/null 2>&1; then | ||
| echo "@sanction/sdk@$VERSION already on npm — skipping npm publish" | ||
| else | ||
| npm publish | ||
| fi |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Serialize publishes for this package.
Two manual runs can both pass npm view; one then fails on npm publish after the other wins. This contradicts the workflow’s idempotency claim. Add a workflow-level concurrency group.
Proposed fix
name: Publish SDK
+
+concurrency:
+ group: publish-sanction-sdk
+ cancel-in-progress: false🧰 Tools
🪛 zizmor (1.26.1)
[info] 39-39: prefer trusted publishing for authentication (use-trusted-publishing): this command
(use-trusted-publishing)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/publish-sdk.yml around lines 37 - 42, Add a workflow-level
concurrency configuration to serialize manual publishes for this package, using
a stable group identifier such as the workflow name and package/ref, and
configure appropriate cancellation behavior so concurrent runs cannot both reach
the npm publish logic.
| // Poll until the owner approves, then retry with the grant: | ||
| const status = await sanction.getAuthorization(decision.requestId) | ||
| if (status.grantId) { | ||
| // retry authorize / authorizeTool with grantId: status.grantId | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Implement polling or describe this as a single status check.
This calls getAuthorization only once. A pending response remains escalated, so the example never reaches a later owner approval or retry. Add bounded polling/backoff, or explicitly tell callers to schedule another check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/README.md` around lines 42 - 46, The README example’s “poll until
approved” wording is inconsistent with its single getAuthorization call. Update
the example around sanction.getAuthorization to either implement bounded polling
with backoff until approval or timeout, or clearly describe it as one status
check and instruct callers to schedule subsequent checks before retrying with
status.grantId.
| const t = sanctionTool(c, "deploy", { execute: async () => "ok" }, { server: "ci", grantId: "grant_1" }) | ||
| expect(await t.execute({})).toBe("ok") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
rg -n -C2 'execute:\s*async\s*\(\)\s*=>\s*"ok"|await t\.execute\(\{\}\)' sdk/src/adapters.test.tsRepository: ericlovold/sanction
Length of output: 565
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '\n== Files ==\n'
git ls-files 'sdk/src/*' | sed -n '1,200p'
printf '\n== Locate sanctionTool ==\n'
rg -n 'function sanctionTool|const sanctionTool|export .*sanctionTool|sanctionTool\(' sdk/src -g '!**/*.map'
printf '\n== Type signatures around adapter tests ==\n'
ast-grep outline sdk/src/adapters.test.ts --view expanded || true
printf '\n== Read relevant sources ==\n'
for f in sdk/src/adapters.ts sdk/src/adapters.test.ts sdk/src/*.ts; do
[ -f "$f" ] || continue
echo "--- $f ($(wc -l < "$f") lines) ---"
sed -n '1,240p' "$f"
doneRepository: ericlovold/sanction
Length of output: 50377
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '\n== package manifests ==\n'
for f in package.json sdk/package.json pnpm-workspace.yaml tsconfig.json sdk/tsconfig.json; do
if [ -f "$f" ]; then
echo "--- $f ---"
sed -n '1,220p' "$f"
fi
done
printf '\n== tsconfig/test inclusion ==\n'
rg -n '"include"|"exclude"|vitest|typecheck|tsc|check' package.json sdk/package.json tsconfig.json sdk/tsconfig.json -nRepository: ericlovold/sanction
Length of output: 5207
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '\n== package manifests ==\n'
for f in package.json sdk/package.json pnpm-workspace.yaml tsconfig.json sdk/tsconfig.json; do
if [ -f "$f" ]; then
echo "--- $f ---"
sed -n '1,220p' "$f"
fi
done
printf '\n== tsconfig/test inclusion ==\n'
rg -n '"include"|"exclude"|vitest|typecheck|tsc|check' package.json sdk/package.json tsconfig.json sdk/tsconfig.jsonRepository: ericlovold/sanction
Length of output: 5207
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
node - <<'JS'
let ts;
try {
ts = require('typescript');
} catch (e) {
console.log("typescript module unavailable:", e.message);
process.exit(0);
}
const source = `
type AiSdkToolLike = {
description?: string
parameters?: unknown
execute?: (args: unknown, options?: unknown) => Promise<unknown> | unknown
[k: string]: unknown
}
declare function sanctionTool<T extends AiSdkToolLike>(
client: unknown,
name: string,
aiTool: T,
opts?: { server?: string; grantId?: string },
): T
const t = sanctionTool({}, "deploy", { execute: async () => "ok" }, { server: "ci", grantId: "grant_1" })
t.execute({})
`;
const fileName = "probe.ts";
const options = {
strict: true,
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.ESNext,
lib: ["lib.es2022.d.ts", "lib.dom.d.ts"],
noEmit: true,
};
const host = ts.createCompilerHost(options);
host.getSourceFile = (f, lang) => {
if (f === fileName) return ts.createSourceFile(f, source, lang, true, ts.ScriptKind.TS);
return ts.createSourceFile(f, "", lang, true);
};
host.readFile = (f) => (f === fileName ? source : "");
host.fileExists = (f) => f === fileName;
host.writeFile = () => {};
const program = ts.createProgram([fileName], options, host);
const diags = ts.getPreEmitDiagnostics(program);
console.log("typescript version:", ts.version);
console.log("diagnostics:");
for (const d of diags) {
const msg = ts.flattenDiagnosticMessageText(d.messageText, "\n");
console.log(`- TS${d.code}: ${msg}`);
}
const checker = program.getTypeChecker();
const sf = program.getSourceFile(fileName);
function findVar(name) {
let found;
ts.forEachChild(sf, function visit(node) {
if (ts.isVariableDeclaration(node) && node.name.getText(sf) === name) found = node;
ts.forEachChild(node, visit);
});
return found;
}
const decl = findVar("t");
if (decl) {
const tType = checker.getTypeAtLocation(decl.name);
const exec = tType.getProperty("execute");
if (exec) {
const execType = checker.getTypeOfSymbolAtLocation(exec, decl.name);
console.log("t.execute type:", checker.typeToString(execType));
}
}
JSRepository: ericlovold/sanction
Length of output: 3285
Make the test tool accept the argument it invokes with. In sdk/src/adapters.test.ts:116-117, sanctionTool preserves T, so the zero-argument execute here does not match t.execute({}) and breaks typechecking.
🧰 Tools
🪛 GitHub Actions: CI / 0_build.txt
[error] 117-117: TypeScript (tsc) error TS2554: Expected 0 arguments, but got 1.
🪛 GitHub Actions: CI / build
[error] 117-117: TypeScript (tsc --noEmit) error TS2554: Expected 0 arguments, but got 1.
🪛 GitHub Check: build
[failure] 117-117:
Expected 0 arguments, but got 1.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk/src/adapters.test.ts` around lines 116 - 117, The test tool’s execute
implementation does not accept the argument passed by t.execute({}). Update the
sanctionTool call in the relevant test to define execute with a compatible input
parameter while preserving its "ok" result, so the inferred tool type matches
the invocation.
Source: Linters/SAST tools
…#193) Bump 0.6.0 → 0.7.0 and drain the truth surfaces (truthsync folded into the cut). The pack since v0.6.0: pay-per-crawl sanctionedFetch (#183), @sanction/sdk publish-ready (#169), Sanction Local install package (#168, #184), observe mode (#187) + observe digest & subtree rollup (#192), email-lands-on-the-decision (#190). Changelog gaps filled (verified against code, not the draft): - v0.7.0 release header (theme: adoption — watch-first, install, drop-in). - Observe mode entry (OBS-1 + OBS-2): the do-nothing on-ramp — real engine records the would-be outcome, blocks nothing, flip-to-enforce per pool with a revisioned timestamp; org roots read the whole subtree. - Email-on-decision entry (#190): escalation notices deep-link to the exact pending request (approveUrlFor), not the generic inbox. Roadmap rotated: Local install → shipped phrasing; observe-mode adoption added to Now as shipped; arc comment updated. (Demo companies, consulting page, and the skills work are infra/site/tooling — no product-changelog entry, by design.) Diff is package.json + changelog + roadmap only. Isolated branch — does not touch the concurrent MCP-demo work. Claude-Session: https://claude.ai/code/session_01UEw1RdHk5bmphKxr6N5mJY Co-authored-by: Claude <noreply@anthropic.com>
Summary
@sanction/sdk@0.6.0publish-ready: public package metadata, FSL-1.1-MIT LICENSE (matches COMMERCIAL-LICENSE), build/prepublishOnly, and a manualpublish-sdkworkflow (idempotent, sameNPM_TOKENpattern as MCP).getAuthorization(requestId)polls for the one-use grant;sanctionToolacceptsgrantIdon retry.Before merging / publishing
sanctionnpm org exists andNPM_TOKENcan publish@sanction/*Test plan
cd sdk && npm run typecheck && npm run buildnpx vitest run sdk/src(47/47)cd sdk && npm pack --dry-runand confirmdist/,LICENSE,README.mdare includednpm view @sanction/sdk version→0.6.0Made with Cursor
Summary by CodeRabbit
New Features
Documentation
Chores