Skip to content
Merged
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: 2 additions & 2 deletions .agents/skills/validate-sas-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ name: validate-sas-release
description: Validate Super Agent Skill changes before publication, using the existing local checks without Lovable generation credits.
---

Run `npm ci` when locked dependencies changed. Run `npm run check:project` from the repository root. This batches tests, types, dependency audit, build, lint regression checking, content validation and both generated-artifact checks. It writes actual exit codes and logs to `artifacts/validation/`; failures block release.
Run `npm ci` when locked dependencies changed. Run `npm run check:project` from the repository root. This batches tests, types, dependency audit, build, strict lint, content validation and both generated-artifact checks. It writes actual exit codes and logs to `artifacts/validation/`; failures block release.

Use `npm run check:project -- --security` with the existing Supabase publishable environment to include RLS and anonymous-access checks. Never expose credentials or weaken allowlists to pass. With a local dev server running, add `--ui` to test signup at mobile and desktop sizes with intercepted auth; review its screenshots personally.

`npm run lint:strict` retains all ESLint errors. The normal lint command permits only the recorded pre-existing explicit-any debt per file; it blocks increases and all other errors. Never increase that baseline to make a change pass. Report outstanding debt explicitly.
`npm run lint` and `npm run lint:strict` both block every ESLint error, including every explicit `any`. No baseline exceptions are permitted. Validate the SDK with `npm run check:types:sdk` as well. Use generated database contracts and validated JSON boundaries; never replace removed types with unchecked assertions or inferred `any`.

Check version-specific trust evidence, query-preserving auth redirects and MCP outcome semantics when those flows change. No model evaluations, signup creation, agent requests, provisioning, paid operations or production writes are part of this batch. Inspect and validate any additive SQL separately. Publish only after checks pass and publication is user-authorized; verify that Lovable's commit matches the validated code before using direct deployment. Preserve approval, signature and evidence gates.
2 changes: 1 addition & 1 deletion docs/testing/ACTIVATION-RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ MCP v2 emits attempted/succeeded/failed/unknown outcomes. HTTP errors, JSON-RPC

`npm run check:project` batches the existing tests, TypeScript, build, ESLint regression gate, content schema validation and generated mirrors/plugin checks. Add `--security` with existing publishable Supabase environment variables for the RLS/public-access probes; add `--ui` with a running local dev server for intercepted-auth desktop/mobile checks. Receipts live in ignored `artifacts/validation/`. Empty/incomplete batches fail. See `.agents/skills/validate-sas-release/SKILL.md`.

The old lint had 11,965 errors: formatting dominated, with 612 existing explicit-any uses. Formatting and other blocking rules were repaired. Those 612 type-debt instances are **not fixed**: `lint` reports them against the frozen original per-file counts in `security/lint-legacy-any.json`, blocks increases and blocks every other error. `lint:strict` retains the original ESLint command and still fails on this debt. Do not increase the baseline. React development/export warnings remain advisory. This adds a practical regression gate to CI, which previously ran only tests; it is not a claim that all typing debt is gone.
The earlier release recorded 612 explicit-any violations. The follow-up removes that allowance: both lint commands now block every ESLint error with zero explicit-any exceptions. The app and SDK have independent TypeScript checks. JSON boundaries validate agent builds, email queue messages and MCP envelopes, while database calls use the generated schema and optional migration contracts. React development/export warnings remain advisory.

## Prepublication evidence

Expand Down
29 changes: 29 additions & 0 deletions docs/testing/TYPE-SAFETY-RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Type safety release validation

The 612 explicit `any` violations recorded by the prior release have been removed. `scripts/lint.mjs` now blocks every ESLint error and writes an aggregate receipt. The per-file allowance file was deleted; ESLint rules remain unchanged. TypeScript now enables `noImplicitAny` as well as `strict`. The generated TanStack router file retains its existing generator-owned exclusions and is unchanged.

Database clients use the generated database contract. `optional-schema.ts` describes the optional feature tables from the committed migrations named in that file; these types do not provision those tables. Read-only inspection through the native Lovable database integration confirmed the corrected live column names: `packages.author_id`, `package_upload_jobs.finished_at`, `mcp_oauth_tokens.token_hash`, and `skill_executions.version` / `error_kind` / required `package_id`.

The exposed contract mismatches were corrected, including package search-vector serialization, version tags coming from the package, comparison runs from `package_metrics_daily`, and tip/fork ownership. Telemetry now uses one authoritative, rate-limited `report_skill_execution` RPC instead of an invalid direct insert followed by a second write.

JSON boundaries validate persisted agent content, email queue messages, auth email fields, and MCP envelopes. Reauthentication requires its token without requiring an unused confirmation URL. Stripe keeps the existing gateway API version; a mocked transport test verifies its version and authentication headers without making a network call. Signature tests cover invalid signatures, stale or nonnumeric timestamps, and malformed envelopes.

Run the reusable batch from the repository root:

```sh
npm run check:project -- --ui --security
```

The security checks use the existing Supabase publishable environment. The UI check requires a local dev server on port 8080 and intercepts auth requests. No real signup, email, charge, model generation, or Lovable agent request is part of validation.

Actual local evidence:

- 198 tests pass, including eight new boundary/regression tests.
- Application and SDK TypeScript checks pass.
- Strict ESLint reports zero errors and zero explicit-any violations across 636 files; existing development warnings remain advisory.
- Production build and dependency audit pass.
- Content, skills mirror, and Agent Plugin artifact checks pass.
- RLS and anonymous-access probes pass across 85 public tables, preserving existing allowlists.
- Signup checks pass at 390px and 1440px; screenshots were visually reviewed.

The batch writes logs, exact exit codes and aggregate results to `artifacts/validation/`. Jev's advisory review is retained there separately; deterministic checks and code inspection decide release readiness. Publication must use the validated Git commit after CI passes and source synchronization is confirmed.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"check:project": "node scripts/check-project.mjs",
"test:ui": "node scripts/check-signup-ui.mjs",
"check:types": "tsc --noEmit",
"check:dependencies": "npm audit --audit-level=high"
"check:dependencies": "npm audit --audit-level=high",
"check:types:sdk": "tsc -p packages/sdk-ts/tsconfig.json --noEmit"
},
"dependencies": {
"@ai-sdk/openai-compatible": "^2.0.47",
Expand Down
19 changes: 9 additions & 10 deletions packages/sdk-ts/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseEnvelope, type RpcEnvelope } from "./protocol";
/**
* @superagentskill/sdk — typed client for the Super Agent Skill MCP server.
*
Expand Down Expand Up @@ -141,7 +142,10 @@ export class SuperAgentSkill {

// ------------------ Internals -----------------------------------------

private async rawRequest(body: unknown, init: { signal?: AbortSignal }): Promise<any> {
private async rawRequest(
body: unknown,
init: { signal?: AbortSignal },
): Promise<RpcEnvelope | null> {
let attempt = 0;
let lastErr: unknown = null;
while (attempt <= this.maxRetries) {
Expand Down Expand Up @@ -262,20 +266,15 @@ function parseRetryAfter(res: Response): number {
return 1;
}

async function safeJson(res: Response): Promise<any> {
async function safeJson(res: Response): Promise<RpcEnvelope | null> {
try {
return await res.json();
return parseEnvelope(await res.text());
} catch {
return null;
}
}

function tryParse(text: string): any {
try {
return JSON.parse(text);
} catch {
return null;
}
function tryParse(text: string): RpcEnvelope | null {
return parseEnvelope(text);
}

/**
Expand Down
35 changes: 35 additions & 0 deletions packages/sdk-ts/src/protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export interface RpcEnvelope {
result?: Record<string, unknown> & { content?: Array<{ type: string; text?: string }> };
error?: { message?: string; code?: number; data?: unknown };
}
const record = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === "object" && !Array.isArray(value);
export function parseEnvelope(text: string): RpcEnvelope | null {
let value: unknown;
try {
value = JSON.parse(text);
} catch {
return null;
}
if (!record(value)) return null;
const out: RpcEnvelope = {};
if (record(value.error)) {
out.error = {
message: typeof value.error.message === "string" ? value.error.message : undefined,
code: typeof value.error.code === "number" ? value.error.code : undefined,
data: value.error.data,
};
}
if (record(value.result)) {
out.result = { ...value.result };
if (Array.isArray(value.result.content))
out.result.content = value.result.content
.filter(record)
.filter((item) => typeof item.type === "string")
.map((item) => ({
type: String(item.type),
text: typeof item.text === "string" ? item.text : undefined,
}));
}
return out;
}
18 changes: 15 additions & 3 deletions packages/sdk-ts/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,23 @@ export class MCPError extends Error {
export class MCPUnauthorizedError extends MCPError {
public readonly authorizationUrl?: string;
public readonly tokensUrl?: string;
constructor(message: string, data?: any) {
constructor(message: string, data?: unknown) {
super(message, { code: -32001, status: 401, data });
this.name = "MCPUnauthorizedError";
this.authorizationUrl = data?.authorization_url;
this.tokensUrl = data?.tokens_url;
this.authorizationUrl =
data &&
typeof data === "object" &&
"authorization_url" in data &&
typeof data.authorization_url === "string"
? data.authorization_url
: undefined;
this.tokensUrl =
data &&
typeof data === "object" &&
"tokens_url" in data &&
typeof data.tokens_url === "string"
? data.tokens_url
: undefined;
}
}

Expand Down
1 change: 1 addition & 0 deletions scripts/check-project.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
const checks = [
"test:all",
"check:types",
"check:types:sdk",
"check:dependencies",
"build",
"lint",
Expand Down
34 changes: 15 additions & 19 deletions scripts/lint.mjs
Original file line number Diff line number Diff line change
@@ -1,36 +1,32 @@
import { ESLint } from "eslint";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { writeFileSync, mkdirSync } from "node:fs";
import { relative } from "node:path";

const baseline = JSON.parse(
readFileSync(new URL("../security/lint-legacy-any.json", import.meta.url), "utf8"),
);
const eslint = new ESLint();
const results = await eslint.lintFiles(["."]);
if (!results.length) throw new Error("No files linted; refusing empty success");
let blocking = 0;
let debt = 0;
let explicitAny = 0;
const report = [];
for (const file of results) {
const path = relative(process.cwd(), file.filePath);
const legacy = file.messages.filter((m) => m.ruleId === "@typescript-eslint/no-explicit-any");
const allowance = baseline[path] ?? 0;
debt += legacy.length;
const other = file.messages.filter(
(m) => m.severity === 2 && m.ruleId !== "@typescript-eslint/no-explicit-any",
);
const excess = Math.max(0, legacy.length - allowance);
blocking += other.length + excess;
if (other.length || excess) console.error(path, other, { newExplicitAny: excess });
if (legacy.length || other.length || excess)
report.push({ path, legacyAny: legacy.length, allowance, blocking: other.length + excess });
const errors = file.messages.filter((m) => m.severity === 2);
const anyCount = file.messages.filter(
(m) => m.ruleId === "@typescript-eslint/no-explicit-any",
).length;
blocking += errors.length;
explicitAny += anyCount;
if (errors.length) {
const path = relative(process.cwd(), file.filePath);
console.error(path, errors);
report.push({ path, explicitAny: anyCount, errors });
}
}
mkdirSync("artifacts/validation", { recursive: true });
writeFileSync(
"artifacts/validation/lint.json",
JSON.stringify({ files: results.length, blocking, legacyAny: debt, report }, null, 2),
JSON.stringify({ files: results.length, blocking, explicitAny, report }, null, 2),
);
console.log(
`Linted ${results.length} files: ${blocking} blocking errors; ${debt} pre-existing explicit-any uses remain (visible debt, not fixed).`,
`Linted ${results.length} files: ${blocking} blocking errors; ${explicitAny} explicit-any violations.`,
);
process.exitCode = blocking ? 1 : 0;
Loading
Loading