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
47 changes: 47 additions & 0 deletions CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,50 @@ Checks:

The kit is dev-dependency friendly: `@hasna/contracts` can be a `devDependency`
and the checks run under `bun test` with no runtime footprint in the app.

---

## 11. Secure local-store lifecycle

The shared secure local-store policy is `hasna.secure_local_store_policy.v1`
(`SecureLocalStorePolicySchema`) and the helper module is
`@hasna/contracts/secure-local-store`.

This policy applies to local operator state under `.hasna` and `.codewith`.
The default inventory is explicit by package: Codewith, Todos, Conversations,
Mementos, Knowledge, Projects, Browser, Terminal, Logs, and Loops.

Local stores **MUST** use owner-only defaults:

- Store directories: `0700`.
- Store files: `0600`.
- SQLite main DB files, WAL sidecars, and SHM sidecars: `0600`.
- Backup, export, report, session, snapshot, tmp, and log artifacts: `0600`
unless a package records a narrower non-secret exception.

Lifecycle cleanup **MUST** default to dry-run. Destructive retention requires:

1. Explicit apply intent.
2. A package-owned retention adapter.
3. Artifact allowlist matches; no broad delete outside the allowlist.
4. Active-record exclusion proof for current tasks, sessions, messages, runs,
workspace rows, attachments, evidence, or other package-owned references.
5. Redaction-before-persistence and redacted evidence from the owning package.

SQLite maintenance **MUST NOT** run against active stores. WAL checkpoint,
incremental vacuum, optimize, or vacuum operations are allowed only when the
caller explicitly proves exclusive/offline access for that package store. The
contracts helper plans maintenance and can apply it only when the caller opts in
to both apply mode and SQLite maintenance.

The CLI surface:

```bash
contracts secure-local-store --json
contracts secure-local-store "$HOME" --json --plan --store todos
contracts secure-local-store "$HOME" --json --apply --store todos
contracts secure-local-store "$HOME" --json --plan --retention --store todos --retention-proof todos-exports-backups
```

The CLI and helper report paths, modes, counts, statuses, store ids, and adapter
ids only. They do not read or print file contents.
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ contracts no-cloud-scan --manifest app-cloud.manifest.json .
contracts no-cloud-scan --json hasna-todos-0.11.62.tgz
```

Print the shared secure local-store policy for `.hasna` and `.codewith`, or
produce a dry-run lifecycle plan for owner-only permissions and package-owned
retention adapters:

```bash
contracts secure-local-store --json
contracts secure-local-store --json --store todos
contracts secure-local-store "$HOME" --json --plan --store todos
contracts secure-local-store "$HOME" --json --apply --store todos
contracts secure-local-store "$HOME" --json --plan --retention --store todos --retention-proof todos-exports-backups
```

`secure-local-store` never reads file contents. The contract covers `0700`
directories, `0600` files, SQLite DB/WAL/SHM sidecars, backups, exports, active
record exclusions, artifact allowlists, and SQLite maintenance gates. Retention
and SQLite maintenance are dry-run by default and require explicit package
adapter proof before deletion or compaction.

## Storage Kit (vendored codegen)

`vendor-kit` stamps a canonical, self-contained Postgres storage kit into a
Expand Down Expand Up @@ -131,15 +149,15 @@ import { expressApiKey, ApiKeyStore } from "@hasna/contracts/auth";
import { createCloudPoolFromEnv } from "./generated/storage-kit"; // vendored kit

const APP = "todos";
const signingSecret = process.env.HASNA_TODOS_API_SIGNING_KEY!; // shared: HASNA_API_SIGNING_KEY
const apiSigningMaterial = process.env.HASNA_TODOS_API_SIGNING_KEY!; // shared: HASNA_API_SIGNING_KEY
const { client } = createCloudPoolFromEnv(APP); // RDS pool (Amendment A1)
const keys = new ApiKeyStore(client);
await keys.ensureSchema(); // idempotent: api_keys table

app.use(
expressApiKey({
app: APP,
signingSecret,
signingSecret: apiSigningMaterial,
isRevoked: keys.isRevoked, // per-request revocation check against RDS
requiredScopes: ["todos:read"], // optional per-mount scope gate
audit: (e) => log.info("api_auth", e), // per-request AUDIT hook (allow + deny)
Expand Down Expand Up @@ -278,6 +296,10 @@ defaults are applied; output aliases such as `EvidenceRef` describe parsed data.
- `hasna.app_cloud_manifest.v1`: app-owned cloud boundary declaration for a
package that uses its own cloud resources, local cache, and conflict policy
without depending on shared `@hasna/cloud` or `open-cloud` runtimes.
- `hasna.secure_local_store_policy.v1`: shared `.hasna`/`.codewith` secure
local-store lifecycle policy with owner-only modes, package inventory,
retention adapters, active-record exclusions, artifact allowlists, and SQLite
maintenance safety gates.
- `hasna.no_cloud_evidence_pack.v1`: prepublish/CI evidence pack for package
manifest, lockfile, source/runtime config, packed artifact, published
metadata, and app-cloud-manifest scans.
Expand Down
91 changes: 91 additions & 0 deletions examples/secure-local-store-policy.valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"schema": "hasna.secure_local_store_policy.v1",
"id": "secure_local_store_policy_example",
"createdAt": "2026-07-06T00:00:00.000Z",
"version": "2026-07-06",
"scope": [".hasna", ".codewith"],
"defaults": {
"directoryMode": "0700",
"fileMode": "0600",
"dryRunDefault": true,
"requireExplicitApply": true,
"includeSqliteSidecars": true,
"redactedEvidenceOnly": true
},
"stores": [
{
"storeId": "todos",
"packageName": "@hasna/todos",
"displayName": "Todos",
"root": ".hasna",
"relativePath": "todos",
"sqliteDatabaseGlobs": ["todos.db"],
"sensitiveFileGlobs": ["todos.db", "todos.db-wal", "todos.db-shm", "exports/**/*", "backups/**/*"],
"backupGlobs": ["backups/**/*"],
"exportGlobs": ["exports/**/*"],
"retentionAdapters": [
{
"id": "todos-exports-backups",
"description": "Redacted backup and export retention for Todos artifacts.",
"ttlDays": 14,
"artifactClasses": ["backup", "export"],
"allowlistGlobs": ["backups/**/*", "exports/**/*"],
"activeRecordExclusions": [
{
"id": "todos-active-evidence",
"source": "sqlite",
"table": "task_files",
"column": "path",
"description": "Exclude files still referenced by active tasks or verification evidence."
}
],
"sqliteMaintenance": {
"safeWhen": "exclusive_access",
"operations": ["wal_checkpoint_truncate", "optimize"]
}
}
]
},
{
"storeId": "codewith",
"packageName": "codewith",
"displayName": "Codewith native state",
"root": ".codewith",
"relativePath": ".",
"sqliteDatabaseGlobs": ["state_*.sqlite", "logs_*.sqlite", "goals_*.sqlite"],
"sensitiveFileGlobs": ["sessions/**/*.jsonl", "shell_snapshots/**/*", "*.sqlite"],
"backupGlobs": ["backups/**/*"],
"exportGlobs": ["exports/**/*"],
"retentionAdapters": [
{
"id": "codewith-session-snapshots",
"description": "Session and shell snapshot retention after package redaction.",
"ttlDays": 30,
"artifactClasses": ["session", "snapshot", "log"],
"allowlistGlobs": ["sessions/**/*.jsonl", "shell_snapshots/**/*", "logs/**/*"],
"activeRecordExclusions": [
{
"id": "codewith-active-session",
"source": "package_adapter",
"description": "Exclude active sessions, leased schedules, active monitors, and active goal rows."
}
],
"sqliteMaintenance": {
"safeWhen": "exclusive_access",
"operations": ["wal_checkpoint_truncate", "optimize"]
}
}
]
}
],
"lifecycle": {
"retentionDryRunDefault": true,
"requireActiveRecordExclusionProof": true,
"requireArtifactAllowlist": true,
"sqliteMaintenanceRequiresExclusiveAccess": true
},
"warnings": [
"Retention defaults to dry-run and requires package-owned active-record checks.",
"SQLite maintenance requires explicit exclusive access."
]
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
"types": "./dist/service-contract.d.ts",
"import": "./dist/service-contract.js"
},
"./secure-local-store": {
"types": "./dist/secure-local-store.d.ts",
"import": "./dist/secure-local-store.js"
},
"./conformance": {
"types": "./dist/conformance.d.ts",
"import": "./dist/conformance.js"
Expand Down Expand Up @@ -74,7 +78,7 @@
"LICENSE"
],
"scripts": {
"build": "rm -rf dist && bun build src/index.ts src/schemas.ts src/validators.ts src/no-cloud.ts src/mode.ts src/service-contract.ts src/conformance.ts src/kit/generate.ts src/auth/index.ts src/sdk/generate.ts --root src --outdir dist --target bun && bun build src/cli/index.ts --outdir dist/cli --target bun && cp src/hasna.contract.schema.json dist/hasna.contract.schema.json && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
"build": "rm -rf dist && bun build src/index.ts src/schemas.ts src/validators.ts src/no-cloud.ts src/mode.ts src/service-contract.ts src/secure-local-store.ts src/conformance.ts src/kit/generate.ts src/auth/index.ts src/sdk/generate.ts --root src --outdir dist --target bun && bun build src/cli/index.ts --outdir dist/cli --target bun && cp src/hasna.contract.schema.json dist/hasna.contract.schema.json && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
"typecheck": "tsc --noEmit",
"test": "bun test",
"lint": "tsc --noEmit",
Expand Down
11 changes: 11 additions & 0 deletions scripts/smoke-dist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ const root = join(import.meta.dir, "..");
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version: string };
const { CONTRACTS_PACKAGE_VERSION } = await import("../dist/schemas.js");
const { scanNoCloudTarget } = await import("../dist/no-cloud.js");
const { secureLocalStorePolicy } = await import("../dist/secure-local-store.js");

if (typeof scanNoCloudTarget !== "function") {
throw new Error("dist/no-cloud.js did not export scanNoCloudTarget");
}
if (typeof secureLocalStorePolicy !== "function") {
throw new Error("dist/secure-local-store.js did not export secureLocalStorePolicy");
}

type CommandResult = ReturnType<typeof Bun.spawnSync>;

Expand Down Expand Up @@ -76,6 +80,13 @@ if (conformancePayload.ok !== true || conformancePayload.failed !== 0 || Number(
throw new Error("conformance did not report a non-empty passing fixture set");
}

const secureStore = run(["secure-local-store", "--json", "--store", "todos"]);
requireExit(secureStore, 0, "secure-local-store");
const secureStorePayload = parseJson(secureStore, "secure-local-store");
if (secureStorePayload.schema !== "hasna.secure_local_store_policy.v1") {
throw new Error("secure-local-store did not emit the secure local-store policy schema");
}

const noCloudDir = mkdtempSync(join(tmpdir(), "contracts-no-cloud-dist-"));
try {
writeFileSync(join(noCloudDir, "package.json"), JSON.stringify({ name: "@hasna/dist-smoke", version: packageJson.version }));
Expand Down
Loading
Loading