@vegastack/skills 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -7
- package/dist/index.js +195 -99
- package/package.json +16 -5
- package/skill/arch-guardian/SKILL.md +84 -0
- package/skill/arch-guardian/agents/openai.yaml +4 -0
- package/skill/arch-guardian/assets/adr-template.md +25 -0
- package/skill/arch-guardian/assets/answers-example.json +10 -0
- package/skill/arch-guardian/assets/architecture-profile.json +13 -0
- package/skill/arch-guardian/assets/architecture-profile.schema.json +31 -0
- package/skill/arch-guardian/references/advisory-report.md +65 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/ai-cost.md +2 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/ai-data-boundaries.md +2 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/ai-evals.md +2 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/connectors-sandbox.md +2 -2
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/delivery-operations.md +5 -5
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/durable-execution.md +3 -1
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/flutter.md +1 -1
- package/skill/arch-guardian/references/architecture/foundation.md +31 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/hosting-reliability.md +1 -1
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/identity-tenancy.md +7 -7
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/model-lifecycle.md +5 -3
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/models-observability.md +1 -1
- package/skill/arch-guardian/references/architecture/security-privacy.md +27 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/topology-monorepo.md +1 -1
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/web.md +1 -1
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/foundation-compatibility.json +1 -1
- package/skill/arch-guardian/references/profile-governance.md +40 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/rule-model.json +2 -2
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/workflows.md +15 -12
- package/skill/{vegastack-arch-guardian → arch-guardian}/refresh/REFRESH.md +10 -3
- package/skill/{vegastack-arch-guardian → arch-guardian}/refresh/sources.json +34 -22
- package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/lib.mjs +1 -10
- package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/profile-tool.mjs +43 -49
- package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/refresh-evidence.mjs +46 -5
- package/skill/arch-guardian/scripts/validate-profile.mjs +65 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/verify-corpus.mjs +1 -13
- package/skill/skill-maintainer/SKILL.md +79 -0
- package/skill/skill-maintainer/agents/openai.yaml +4 -0
- package/skill/skill-maintainer/references/release-ops.md +63 -0
- package/skill/skill-maintainer/references/standards.md +92 -0
- package/skill/skill-maintainer/refresh/REFRESH.md +40 -0
- package/skill/skill-maintainer/refresh/sources.json +103 -0
- package/skill/skillify/SKILL.md +154 -0
- package/skill/skillify/agents/openai.yaml +4 -0
- package/skill/skillify/assets/templates/README.md.template +26 -0
- package/skill/skillify/assets/templates/REFRESH.md.template +21 -0
- package/skill/skillify/assets/templates/SKILL.md.template +26 -0
- package/skill/skillify/assets/templates/openai.yaml.template +4 -0
- package/skill/skillify/assets/templates/skill.test.ts.template +40 -0
- package/skill/skillify/assets/templates/sources.json.template +6 -0
- package/skill/skillify/references/authoring.md +72 -0
- package/skill/skillify/references/eval-playbook.md +62 -0
- package/skill/skillify/refresh/REFRESH.md +24 -0
- package/skill/skillify/refresh/sources.json +6 -0
- package/skill/skillify/scripts/scaffold-skill.mjs +138 -0
- package/skill-integrity.json +74 -45
- package/skill/vegastack-arch-guardian/SKILL.md +0 -96
- package/skill/vegastack-arch-guardian/agents/openai.yaml +0 -4
- package/skill/vegastack-arch-guardian/assets/adr-template.md +0 -40
- package/skill/vegastack-arch-guardian/assets/answers-example.json +0 -20
- package/skill/vegastack-arch-guardian/assets/architecture-profile.json +0 -25
- package/skill/vegastack-arch-guardian/assets/architecture-profile.schema.json +0 -213
- package/skill/vegastack-arch-guardian/references/architecture/foundation.md +0 -33
- package/skill/vegastack-arch-guardian/references/architecture/security-privacy.md +0 -23
- package/skill/vegastack-arch-guardian/references/control-catalog.json +0 -55
- package/skill/vegastack-arch-guardian/references/profile-governance.md +0 -54
- package/skill/vegastack-arch-guardian/scripts/architecture-check.mjs +0 -323
- package/skill/vegastack-arch-guardian/scripts/validate-profile.mjs +0 -241
- /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/deployment-review-template.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/service-design-template.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/threat-model-template.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/agent-product.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/data-memory.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/realtime-channels.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/references/golden-patterns.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/schema-validate.mjs +0 -0
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": 1,
|
|
3
|
-
"description": "Atomic checker/validator controls. Normative prose remains canonical in architecture references.",
|
|
4
|
-
"controls": [
|
|
5
|
-
{ "rule": "FOUND-001", "idPattern": "^profile\\.(?:present|parse|schema|schema-version|confirmed|project-name)$", "classification": "invariant", "activation": "profile validation or CI", "verification": "structural/semantic", "waiver": "project-adr" },
|
|
6
|
-
{ "rule": "FOUND-002", "idPattern": "^exception\\.(?:validity|scope-match)$", "classification": "invariant", "activation": "exception declared", "verification": "semantic/filesystem", "waiver": "project-adr" },
|
|
7
|
-
{ "rule": "FOUND-004", "idPattern": "^(?:capability\\.[A-Za-z]+\\.(?:source-root|undeclared|missing-code)|profile\\.capability-alignment)$", "classification": "invariant", "activation": "capability declared or observed", "verification": "static/semantic", "waiver": "project-adr" },
|
|
8
|
-
{ "rule": "PKG-003", "idPattern": "^(?:foundation\\.(?:baseline|adoption|non-supported-adoption)|version\\.[A-Za-z]+\\.[A-Za-z0-9]+)$", "classification": "invariant", "activation": "versioned capability enabled", "verification": "semantic", "waiver": "project-adr" },
|
|
9
|
-
{ "rule": "RUN-001", "idPattern": "^(?:runtime\\.web-bundle-boundary|hosting\\.agents-placement)$", "classification": "forbidden-design", "activation": "owned agents", "verification": "static/semantic", "waiver": "project-adr" },
|
|
10
|
-
{ "rule": "RUN-002", "idPattern": "^(?:runtime\\.web-bundle-boundary|hosting\\.jobs-placement)$", "classification": "forbidden-design", "activation": "owned jobs", "verification": "static/semantic", "waiver": "project-adr" },
|
|
11
|
-
{ "rule": "RUN-003", "idPattern": "^(?:capability\\.source-root-overlap|contract\\.[A-Za-z]+\\.qualification)$", "classification": "invariant", "activation": "provider boundary or shared/external capability", "verification": "semantic/manual", "waiver": "project-adr" },
|
|
12
|
-
{ "rule": "HOST-004", "id": "hosting.web-placement", "classification": "invariant", "activation": "Cloudflare/OpenNext web", "verification": "semantic", "waiver": "project-adr" },
|
|
13
|
-
{ "rule": "MOB-001", "id": "flutter.oauth-pkce", "classification": "invariant", "activation": "Flutter enabled", "verification": "semantic/runtime", "waiver": "project-adr" },
|
|
14
|
-
{ "rule": "MOB-002", "idPattern": "^flutter\\.(?:api-provider|generated-client)$", "classification": "invariant", "activation": "Flutter enabled", "verification": "semantic/generated-contract", "waiver": "project-adr" },
|
|
15
|
-
{ "rule": "DUR-001", "idPattern": "^(?:agents\\.(?:durable-owner|storage-ownership|dual-workflow-state)|jobs\\.workflow-ownership)$", "classification": "invariant", "activation": "agents enabled or observed", "verification": "static/semantic/runtime", "waiver": "project-adr" },
|
|
16
|
-
{ "rule": "DUR-003", "idPattern": "^(?:agents\\.(?:admission|pg-boss-ownership)|jobs\\.database-owner)$", "classification": "invariant", "activation": "agent admission or owned jobs", "verification": "static/semantic/runtime", "waiver": "project-adr" },
|
|
17
|
-
{ "rule": "DATA-002", "idPattern": "^knowledge\\.(?:storage-owner|object-storage-owner)$", "classification": "preferred-default", "activation": "owned knowledge", "verification": "semantic/runtime", "waiver": "project-adr" },
|
|
18
|
-
{ "rule": "SBX-001", "id": "sandbox.activation", "classification": "invariant", "activation": "untrusted execution", "verification": "semantic/runtime", "waiver": "project-adr" },
|
|
19
|
-
{ "rule": "SBX-002", "idPattern": "^sandbox\\.(?:trusted-broker|credentials)$", "classification": "invariant", "activation": "sandbox enabled", "verification": "semantic/runtime", "waiver": "project-adr" },
|
|
20
|
-
{ "rule": "SBX-003", "id": "sandbox.egress", "classification": "forbidden-design", "activation": "sandbox enabled or observed", "verification": "static/runtime", "waiver": "project-adr" },
|
|
21
|
-
{ "rule": "AUTH-001", "id": "auth.organization-boundary", "classification": "invariant", "activation": "authenticated multi-tenant product", "verification": "static/runtime", "waiver": "project-adr" },
|
|
22
|
-
{ "rule": "AUTH-003", "idPattern": "^(?:auth\\.(?:secure-cookies|secure-cookies-code|library|csrf-origin|trusted-origins))$", "classification": "invariant", "activation": "authenticated web", "verification": "static/semantic/runtime", "waiver": "project-adr" },
|
|
23
|
-
{ "rule": "AUTH-004", "idPattern": "^auth\\.(?:pkce|oauth-provider)$", "classification": "invariant", "activation": "delegated client", "verification": "static/runtime", "waiver": "project-adr" },
|
|
24
|
-
{ "rule": "AUTH-005", "id": "auth.api-key-scope", "classification": "forbidden-design", "activation": "API keys observed", "verification": "static/runtime", "waiver": "project-adr" },
|
|
25
|
-
{ "rule": "AUTH-006", "idPattern": "^identity\\.(?:scim-deprovisioning|scim-storage)$", "classification": "invariant", "activation": "SCIM enabled or observed", "verification": "static/semantic/runtime", "waiver": "project-adr" },
|
|
26
|
-
{ "rule": "API-002", "id": "api.generated-contract", "classification": "invariant", "activation": "Flutter/public client enabled", "verification": "semantic/generated-contract", "waiver": "project-adr" },
|
|
27
|
-
{ "rule": "TEN-001", "idPattern": "^tenancy\\.composite-(?:primary-key|relationship)$", "classification": "invariant", "activation": "shared-schema multi-tenancy", "verification": "static/runtime", "waiver": "project-adr" },
|
|
28
|
-
{ "rule": "TEN-002", "idPattern": "^tenancy\\.rls-(?:enable|force|policy|evidence)$", "classification": "invariant", "activation": "shared-schema multi-tenancy", "verification": "static/runtime", "waiver": "project-adr" },
|
|
29
|
-
{ "rule": "TEN-003", "idPattern": "^tenancy\\.(?:context-local|role-bypassrls|context-evidence)$", "classification": "forbidden-design", "activation": "shared-schema multi-tenancy or tenant context observed", "verification": "static/runtime", "waiver": "project-adr" },
|
|
30
|
-
{ "rule": "TEN-004", "id": "tenancy.security-definer", "classification": "forbidden-design", "activation": "privileged shared-schema path", "verification": "static/runtime", "waiver": "project-adr" },
|
|
31
|
-
{ "rule": "SEC-002", "idPattern": "^(?:secret\\.(?:plaintext|bearer)|secrets\\.(?:activation|production-provider|dependency))$", "classification": "forbidden-design", "activation": "production secrets declared or observed", "verification": "static/semantic/runtime", "waiver": "project-adr" },
|
|
32
|
-
{ "rule": "OBS-002", "id": "telemetry.sensitive-data", "classification": "forbidden-design", "activation": "telemetry call observed", "verification": "static/runtime", "waiver": "project-adr" },
|
|
33
|
-
{ "rule": "RT-002", "id": "realtime.websocket-trigger", "classification": "preferred-default", "activation": "WebSockets observed", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
34
|
-
{ "rule": "RT-005", "id": "notifications.durable-intent-owner", "classification": "invariant", "activation": "notifications enabled", "verification": "semantic/runtime", "waiver": "project-adr" },
|
|
35
|
-
{ "rule": "REL-001", "id": "reliability.objectives", "classification": "invariant", "activation": "production project", "verification": "declared/manual", "waiver": "project-adr" },
|
|
36
|
-
{ "rule": "DEL-001", "idPattern": "^(?:inspection\\.(?:traversal|read)|qualification\\.runtime)$", "classification": "invariant", "activation": "review or qualification", "verification": "static/manual/runtime", "waiver": "project-adr" },
|
|
37
|
-
{ "rule": "DUR-009", "id": "agents.exit-contract", "classification": "invariant", "activation": "owned durable execution in production", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
38
|
-
{ "rule": "EVAL-001", "id": "evals.definition", "classification": "invariant", "activation": "model-backed behavior shipped to users", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
39
|
-
{ "rule": "EVAL-002", "id": "evals.promotion-gate", "classification": "invariant", "activation": "agent/prompt/model change promoted", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
40
|
-
{ "rule": "EVAL-003", "id": "evals.dataset-versioning", "classification": "invariant", "activation": "eval datasets exist", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
41
|
-
{ "rule": "EVAL-004", "id": "evals.online-sampling", "classification": "invariant", "activation": "production eval sampling enabled", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
42
|
-
{ "rule": "MLIFE-001", "id": "models.pinned-ids", "classification": "invariant", "activation": "external model providers enabled", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
43
|
-
{ "rule": "MLIFE-002", "id": "models.deprecation-watch", "classification": "invariant", "activation": "external model providers enabled", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
44
|
-
{ "rule": "MLIFE-003", "id": "models.canary-rollout", "classification": "preferred-default", "activation": "prompt+model behavior change", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
45
|
-
{ "rule": "MLIFE-004", "id": "models.backpressure", "classification": "invariant", "activation": "external model providers enabled", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
46
|
-
{ "rule": "COST-001", "id": "cost.workspace-attribution", "classification": "invariant", "activation": "metered AI resources exist", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
47
|
-
{ "rule": "COST-002", "id": "cost.budgets-alerts", "classification": "invariant", "activation": "production AI features", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
48
|
-
{ "rule": "COST-003", "id": "cost.cache-economics", "classification": "preferred-default", "activation": "prompt/context caching enabled", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
49
|
-
{ "rule": "COST-004", "id": "cost.regression-gate", "classification": "invariant", "activation": "AI feature deploy", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
50
|
-
{ "rule": "PII-001", "id": "pii.ingestion-redaction", "classification": "invariant", "activation": "knowledge or embedding ingestion", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
51
|
-
{ "rule": "PII-002", "id": "pii.prompt-boundary", "classification": "invariant", "activation": "external model calls with personal data", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
52
|
-
{ "rule": "PII-003", "id": "pii.output-moderation", "classification": "invariant", "activation": "public-facing AI output", "verification": "manual-qualification", "waiver": "project-adr" },
|
|
53
|
-
{ "rule": "PII-004", "id": "pii.injection-mitigations", "classification": "invariant", "activation": "tool results, retrieval, or connector content in prompts", "verification": "manual-qualification", "waiver": "project-adr" }
|
|
54
|
-
]
|
|
55
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# Profile and governance
|
|
2
|
-
|
|
3
|
-
The v3 profile declares project facts without assuming a full-stack product. `capabilities` is authoritative intent; repository detection is evidence for drift, not a reason to silently enable anything.
|
|
4
|
-
|
|
5
|
-
## Capability ownership
|
|
6
|
-
|
|
7
|
-
- `owned`: the project owns source, deployment, operations and migration. Enabled owned capabilities declare exact versions, placement and source roots.
|
|
8
|
-
- `shared-managed`: another VegaStack owner runs the capability for this project.
|
|
9
|
-
- `external-managed`: a third party runs it behind a project-approved contract.
|
|
10
|
-
- `not-applicable`: use only for disabled capabilities.
|
|
11
|
-
|
|
12
|
-
Enabled shared/external capabilities declare owner/service, contract name/version, tenant/security boundary, identity/audience, data/residency, SLO/recovery dependency, incident ownership, compatibility and migration/exit behavior. They do not require provider source roots in the consumer repository.
|
|
13
|
-
|
|
14
|
-
Owned durable capabilities declare concrete owners in `controls`: agents name `workflowDatabaseOwner` and `agentRunOwner`; jobs name `databaseOwner`; owned knowledge names `postgresOwner` and, when `binaryObjects` is true, `objectStorageOwner`; notifications name `durableIntentOwner`. Credential-bearing connectors/model routing set `credentialBearing: true`, and SCIM activates production secret custody. These are confirmed project facts, never generated placeholders.
|
|
15
|
-
|
|
16
|
-
Never invent owner, SLO, retention, residency, compliance or version values. Missing material facts remain validation errors or `NOT VERIFIED` evidence.
|
|
17
|
-
|
|
18
|
-
## Capability activation
|
|
19
|
-
|
|
20
|
-
Apply rules only when activated by declared intent or observed drift. Key implications:
|
|
21
|
-
|
|
22
|
-
- Flutter requires delegated OAuth/OIDC code with S256 PKCE and generated REST/OpenAPI client consumption.
|
|
23
|
-
- Agents require qualified EVE/Postgres World plus AgentRun.
|
|
24
|
-
- Owned agent admission requires pg-boss; shared admission requires an explicit qualified contract.
|
|
25
|
-
- Untrusted execution requires a sandbox and trusted capability broker.
|
|
26
|
-
- SCIM requires organization mapping and complete deprovisioning.
|
|
27
|
-
- Cloudflare/OpenNext plus owned agents/jobs requires external long-running Node/OCI placement.
|
|
28
|
-
- Removing a capability requires a cleanup/migration plan for durable data, credentials, queues and contracts.
|
|
29
|
-
- Production secrets activate OpenBao unless an explicit shared/external secrets contract is declared.
|
|
30
|
-
|
|
31
|
-
## Exceptions
|
|
32
|
-
|
|
33
|
-
Every project rule is waivable by the project owner. A valid exception declares an exact single rule, exact repository-relative evidence paths, project owner, rationale/decision, risks, compensating controls, verification, rollback/migration, review date or event, and acknowledgement of foundation deviation. `controls` is optional: an exception that omits it covers all controls under its single rule; one that declares control IDs suppresses only the controls it lists. Its contained ADR repeats the identity and decision.
|
|
34
|
-
|
|
35
|
-
Static exceptions match only when rule and exact evidence path match and, when `controls` is declared, the control ID also matches. Wildcards and directory-prefix suppression are forbidden. An exception that lists controls never suppresses a finding for a control it does not list, and no exception covers a second rule. Manual exceptions declare `verificationType: manual-qualification` and remain visibly accepted risk.
|
|
36
|
-
|
|
37
|
-
Outcome semantics:
|
|
38
|
-
|
|
39
|
-
| Outcome | Meaning | CI |
|
|
40
|
-
|---|---|---|
|
|
41
|
-
| `PASS` | recommendation satisfied | pass |
|
|
42
|
-
| `FAIL` | violation or invalid/expired/mismatched exception | fail |
|
|
43
|
-
| `EXCEPTED` | valid active project-owner accepted risk; recommendation remains unmet | pass |
|
|
44
|
-
| `NOT VERIFIED` | environment behavior not reproduced; reason/risk/owner/next action required | configurable warning |
|
|
45
|
-
|
|
46
|
-
The guardian may still state `GUARDIAN VERDICT: REJECT` when an accepted risk is unsafe. Foundation evolution changes the recommended baseline; project exceptions remain distinguishable and do not silently expire on source drift.
|
|
47
|
-
|
|
48
|
-
## Governance operations
|
|
49
|
-
|
|
50
|
-
- Evidence labels are optional; they are reserved for review and drift reports and never affect exception matching.
|
|
51
|
-
- Drift PRs from the automated source refresh are reviewed by whoever merges them; drift policy has no per-topic owners.
|
|
52
|
-
- Source staleness thresholds are a minimum of 14 days, aligned to the weekly automated refresh; shorter thresholds only produce false staleness between refreshes.
|
|
53
|
-
- Profiles pin the foundation version (currently `0.3.0`); it is decoupled from the installer npm package version, so installer upgrades never change a project's pinned foundation.
|
|
54
|
-
- `DUR-007`-style full qualification matrices are required before the first paying tenant, not before every merge; interim merges record `NOT VERIFIED` with reason, owner, and next action.
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readFile, readdir } from 'node:fs/promises'
|
|
3
|
-
import { basename, dirname, extname, relative as relativePath, resolve } from 'node:path'
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
-
import { issue, pathExists, resolveProfile } from './lib.mjs'
|
|
6
|
-
import { canonicalRuleIds, validateProfile } from './validate-profile.mjs'
|
|
7
|
-
|
|
8
|
-
const scriptDirectory = dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
const scanExtensions = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs', '.json', '.yaml', '.yml', '.toml', '.sql', '.md', '.py', '.go', '.java', '.kt', '.dart', '.rs', '.php', '.rb'])
|
|
10
|
-
const scanNames = new Set(['.env', '.env.local', '.env.production', 'Dockerfile'])
|
|
11
|
-
const ignored = new Set(['node_modules', '.git', '.turbo', 'dist', 'build', '.next', '.agents', '.claude', '.vegastack', 'coverage', 'vendor', 'out', '.output', '.vercel', '.wrangler'])
|
|
12
|
-
// Files this large or with lines this long are treated as generated bundles, not reviewable evidence.
|
|
13
|
-
const bundleBodyBytes = 500_000
|
|
14
|
-
const bundleLineLength = 5_000
|
|
15
|
-
const findingsCapPerControl = 25
|
|
16
|
-
const capabilityPatterns = {
|
|
17
|
-
webControlPlane: /(?:from\s+['"]next(?:\/|['"])|next\.config|@opennextjs\/cloudflare)/i,
|
|
18
|
-
flutter: /(?:pubspec\.yaml|package:flutter|flutter_secure_storage)/i,
|
|
19
|
-
agents: /(?:from\s+['"]eve['"]|@workflow\/world|AgentRun)/i,
|
|
20
|
-
jobs: /(?:from\s+['"]pg-boss['"]|\bPgBoss\b)/i,
|
|
21
|
-
sandbox: /(?:@cloudflare\/sandbox|enableInternet|block_network|SandboxProvider)/i,
|
|
22
|
-
connectors: /(?:\bMCP\b|webhook|connector|refresh_token)/i,
|
|
23
|
-
knowledge: /(?:pgvector|vector\(|\bknowledge\b|embedding)/i,
|
|
24
|
-
modelRouting: /(?:generateText|streamText|AI Gateway|BYOK|model provider)/i,
|
|
25
|
-
enterpriseIdentity: /(?:\bSCIM\b|\bSSO\b|saml)/i,
|
|
26
|
-
realtime: /(?:EventSource|text\/event-stream|WebSocket)/i,
|
|
27
|
-
notifications: /(?:firebase_messaging|\bAPNs\b|notification intent)/i,
|
|
28
|
-
secrets: /(?:OPENBAO|VAULT_ADDR|CLIENT_SECRET|API_KEY|DATABASE_URL)/i
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function loadGuardianIgnore(root) {
|
|
32
|
-
const path = resolve(root, '.guardianignore')
|
|
33
|
-
if (!await pathExists(path)) return []
|
|
34
|
-
return (await readFile(path, 'utf8'))
|
|
35
|
-
.split('\n')
|
|
36
|
-
.map(line => line.trim())
|
|
37
|
-
.filter(line => line && !line.startsWith('#'))
|
|
38
|
-
.map(line => line.replace(/\/+$/, ''))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Traversal skips (rather than aborts on) symlinks, agent-skill trees, and .guardianignore
|
|
42
|
-
// prefixes; the caller reports skips as a single NOT_VERIFIED finding.
|
|
43
|
-
async function sourceFiles(root, ignorePrefixes, skipped) {
|
|
44
|
-
const files = []
|
|
45
|
-
async function walk(directory) {
|
|
46
|
-
const entries = await readdir(directory, { withFileTypes: true })
|
|
47
|
-
if (directory !== root && entries.some(entry => entry.name === 'SKILL.md')) {
|
|
48
|
-
skipped.skillTrees.push(relativePath(root, directory).split('\\').join('/'))
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
if (ignored.has(entry.name)) continue
|
|
53
|
-
const path = resolve(directory, entry.name)
|
|
54
|
-
const relative = relativePath(root, path).split('\\').join('/')
|
|
55
|
-
if (ignorePrefixes.some(prefix => relative === prefix || relative.startsWith(`${prefix}/`))) continue
|
|
56
|
-
if (entry.isSymbolicLink()) { skipped.symlinks.push(relative); continue }
|
|
57
|
-
if (entry.isDirectory()) await walk(path)
|
|
58
|
-
else if (entry.isFile() && (scanExtensions.has(extname(path)) || scanNames.has(basename(path)))) files.push(path)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
await walk(root)
|
|
62
|
-
return files.sort()
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function looksGenerated(body) {
|
|
66
|
-
if (body.length > bundleBodyBytes) return true
|
|
67
|
-
let lineStart = 0
|
|
68
|
-
for (let index = 0; index <= body.length; index += 1) {
|
|
69
|
-
if (index === body.length || body[index] === '\n') {
|
|
70
|
-
if (index - lineStart > bundleLineLength) return true
|
|
71
|
-
lineStart = index + 1
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const lineOf = (body, index) => body.slice(0, Math.max(0, index)).split('\n').length
|
|
78
|
-
const evidence = (path, body, index = 0, type = 'static-sentinel') => ({ path, line: lineOf(body, index), type })
|
|
79
|
-
const enabled = (profile, name) => profile?.capabilities?.[name]?.status === 'enabled'
|
|
80
|
-
const owned = (profile, name) => enabled(profile, name) && profile.capabilities[name].ownership === 'owned'
|
|
81
|
-
const roots = (profile, name) => profile?.capabilities?.[name]?.sourceRoots ?? []
|
|
82
|
-
const inRoots = (path, selected = []) => selected.some(root => path === root || path.startsWith(`${root.replace(/\/$/, '')}/`))
|
|
83
|
-
const moduleSpecifiers = body => [...body.matchAll(/(?:from\s+|require\(\s*|import\(\s*)['"]([^'"]+)['"]/g)].map(match => match[1])
|
|
84
|
-
const nonProductionPath = path => path.split('/').some(part => ['test', 'tests', '__tests__', 'fixture', 'fixtures', 'example', 'examples', 'docs'].includes(part.toLowerCase()))
|
|
85
|
-
const normalizedSqlName = name => name.replaceAll('"', '').toLowerCase()
|
|
86
|
-
|
|
87
|
-
function tenantTables(body) {
|
|
88
|
-
return [...body.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w."-]+)\s*\(([\s\S]*?)\);/gi)]
|
|
89
|
-
.filter(match => /\bworkspace_id\b/i.test(match[2]))
|
|
90
|
-
.map(match => ({ name: match[1].replaceAll('"', ''), definition: match[2], index: match.index ?? 0 }))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function tablePolicies(sqlFiles) {
|
|
94
|
-
const policies = new Map()
|
|
95
|
-
for (const file of sqlFiles) for (const match of file.body.matchAll(/CREATE\s+POLICY\b[\s\S]*?;/gi)) {
|
|
96
|
-
const statement = match[0]
|
|
97
|
-
const table = statement.match(/\bON\s+(?:ONLY\s+)?([\w."-]+)/i)?.[1]
|
|
98
|
-
if (!table) continue
|
|
99
|
-
const key = normalizedSqlName(table)
|
|
100
|
-
if (!policies.has(key)) policies.set(key, [])
|
|
101
|
-
policies.get(key).push({ statement, path: file.path, body: file.body, index: match.index ?? 0 })
|
|
102
|
-
}
|
|
103
|
-
return policies
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const tenantClause = (statement, clause) => new RegExp(`\\b${clause}\\s*\\([\\s\\S]*?(?:workspace_id|current_setting)`, 'i').test(statement) && !new RegExp(`\\b${clause}\\s*\\(\\s*true\\s*\\)`, 'i').test(statement)
|
|
107
|
-
|
|
108
|
-
function rawFinding(findings, rule, control, message, path, body = '', index = 0, extra = {}) {
|
|
109
|
-
findings.push(issue('FAIL', rule, control, message, evidence(path, body, index), extra))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function applyExceptions(findings, exceptionStates, profilePath) {
|
|
113
|
-
const matched = new Set()
|
|
114
|
-
const output = findings.map(finding => {
|
|
115
|
-
const exceptionType = finding.status === 'FAIL' ? 'static-sentinel' : finding.status === 'NOT_VERIFIED' ? 'manual-qualification' : null
|
|
116
|
-
if (!exceptionType || finding.exceptionEligible === false) return finding
|
|
117
|
-
// An exception without a controls list covers every control under its single rule; paths stay exact.
|
|
118
|
-
const state = exceptionStates.find(item => item.valid && item.exception.verificationType === exceptionType && item.exception.ruleId === finding.rule && (!item.exception.controls || item.exception.controls.includes(finding.control)) && item.exception.paths.includes(finding.path))
|
|
119
|
-
if (!state) return finding
|
|
120
|
-
matched.add(state.exception.id)
|
|
121
|
-
return { ...finding, status: 'EXCEPTED', severity: 'accepted-risk', exceptionId: state.exception.id, acceptedBy: state.exception.projectOwner, message: `${finding.message} — accepted project risk; foundation recommendation remains unmet` }
|
|
122
|
-
})
|
|
123
|
-
for (const state of exceptionStates.filter(item => item.valid && !matched.has(item.exception.id))) output.push(issue('FAIL', 'FOUND-002', 'exception.scope-match', `${state.exception.id} did not match an eligible emitted finding for its rule/path/verification scope (controls, when listed, must also match exactly)`, { path: profilePath, line: 1, type: 'declared-control' }, { verificationType: 'semantic', exceptionEligible: false }))
|
|
124
|
-
return output
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function capFindings(findings) {
|
|
128
|
-
const groups = new Map()
|
|
129
|
-
const output = []
|
|
130
|
-
for (const finding of findings) {
|
|
131
|
-
if (finding.status !== 'FAIL') { output.push(finding); continue }
|
|
132
|
-
const key = `${finding.rule}/${finding.control}`
|
|
133
|
-
const group = groups.get(key) ?? { kept: 0, suppressed: 0, last: null }
|
|
134
|
-
groups.set(key, group)
|
|
135
|
-
if (group.kept < findingsCapPerControl) {
|
|
136
|
-
group.kept += 1
|
|
137
|
-
group.last = finding
|
|
138
|
-
output.push(finding)
|
|
139
|
-
} else {
|
|
140
|
-
group.suppressed += 1
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
for (const group of groups.values()) if (group.suppressed > 0 && group.last) group.last.suppressedCount = group.suppressed
|
|
144
|
-
return output
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function checkArchitecture(projectRoot, options = {}) {
|
|
148
|
-
const root = resolve(projectRoot)
|
|
149
|
-
const findings = []
|
|
150
|
-
const discovered = options.profile ? null : await resolveProfile(root)
|
|
151
|
-
const profileRelative = options.profile ?? discovered?.relative ?? '.vegastack/architecture.json'
|
|
152
|
-
const profilePath = resolve(root, profileRelative)
|
|
153
|
-
const legacyProfileName = options.profile ? profileRelative.endsWith('.yaml') : Boolean(discovered?.legacy)
|
|
154
|
-
let profile = null
|
|
155
|
-
let exceptionStates = []
|
|
156
|
-
if (!await pathExists(profilePath)) rawFinding(findings, 'FOUND-001', 'profile.present', 'Commit a confirmed v3 architecture profile for CI; read-only review continued without mutation', profileRelative)
|
|
157
|
-
else {
|
|
158
|
-
const validation = await validateProfile(profilePath, { projectRoot: root, now: options.now })
|
|
159
|
-
profile = validation.profile
|
|
160
|
-
exceptionStates = validation.exceptions
|
|
161
|
-
for (const diagnostic of validation.diagnostics) rawFinding(findings, diagnostic.rule, diagnostic.control, diagnostic.message, diagnostic.path, '', 0, { exceptionEligible: diagnostic.exceptionEligible })
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const skipped = { symlinks: [], skillTrees: [], generated: [] }
|
|
165
|
-
let files = []
|
|
166
|
-
try { files = await sourceFiles(root, await loadGuardianIgnore(root), skipped) } catch (error) {
|
|
167
|
-
rawFinding(findings, 'DEL-001', 'inspection.traversal', `Cannot complete repository traversal: ${error.message}`, '.')
|
|
168
|
-
}
|
|
169
|
-
const content = []
|
|
170
|
-
const observed = Object.fromEntries(Object.keys(capabilityPatterns).map(name => [name, []]))
|
|
171
|
-
for (const path of files) {
|
|
172
|
-
if (resolve(path) === profilePath) continue
|
|
173
|
-
const relative = relativePath(root, path).split('\\').join('/')
|
|
174
|
-
let body
|
|
175
|
-
try { body = await readFile(path, 'utf8') } catch (error) { rawFinding(findings, 'DEL-001', 'inspection.read', `Cannot inspect file: ${error.message}`, relative); continue }
|
|
176
|
-
if (looksGenerated(body)) { skipped.generated.push(relative); continue }
|
|
177
|
-
content.push({ path: relative, body, extension: extname(path) })
|
|
178
|
-
const searchable = `${relative}\n${body}`
|
|
179
|
-
const machineSource = extname(path) !== '.md'
|
|
180
|
-
const productionEvidence = machineSource && !nonProductionPath(relative)
|
|
181
|
-
if (productionEvidence) for (const [name, pattern] of Object.entries(capabilityPatterns)) if (pattern.test(searchable)) observed[name].push(relative)
|
|
182
|
-
|
|
183
|
-
const localSecretAllowance = relative === '.env.local' && profile?.environments?.localDevelopment?.allowances?.includes('local-secrets')
|
|
184
|
-
if (productionEvidence && !localSecretAllowance) {
|
|
185
|
-
for (const match of body.matchAll(/^\s*(?:export\s+)?(?:const\s+)?([A-Z][A-Z0-9_]*(?:SECRET(?:_KEY)?|PASSWORD|PRIVATE_KEY|TOKEN|API_KEY)|API_KEY|CLIENT_SECRET|REFRESH_TOKEN|OPENBAO_TOKEN|DATABASE_URL|BYOK)\s*[:=]\s*(?!process\.env|env\.|os\.environ|System\.getenv|\$\{|['"]?REDACTED)([^\s,;]{8,}|['"][^'"$]{8,}['"])/gim)) rawFinding(findings, 'SEC-002', 'secret.plaintext', 'Probable plaintext credential', relative, body, match.index)
|
|
186
|
-
for (const match of body.matchAll(/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g)) rawFinding(findings, 'SEC-002', 'secret.plaintext', 'Probable plaintext private key', relative, body, match.index)
|
|
187
|
-
for (const match of body.matchAll(/authorization\s*:\s*['"]Bearer\s+[^$'"]{8,}/gi)) rawFinding(findings, 'SEC-002', 'secret.bearer', 'Hard-coded bearer credential', relative, body, match.index)
|
|
188
|
-
for (const match of body.matchAll(/(console\.(?:log|info|warn|error)|logger\.(?:debug|info|warn|error))\s*\([^\n]*(?:TOKEN|SECRET|PASSWORD|DATABASE_URL|API_KEY|AUTHORIZATION|COOKIE|PROMPT|RESTRICTED)/gi)) rawFinding(findings, 'OBS-002', 'telemetry.sensitive-data', 'Probable credential or restricted content emitted to telemetry', relative, body, match.index)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const sandboxApplicable = productionEvidence && (enabled(profile, 'sandbox') || observed.sandbox.length > 0 || capabilityPatterns.sandbox.test(searchable))
|
|
192
|
-
if (sandboxApplicable) for (const match of body.matchAll(/enableInternet\s*[:=]\s*true|block_network\s*[:=]\s*false|allowedHosts\s*[:=]\s*\[\s*['"]\*['"]/g)) rawFinding(findings, 'SBX-003', 'sandbox.egress', 'Production sandbox egress must default deny without wildcards', relative, body, match.index)
|
|
193
|
-
|
|
194
|
-
const tenancyApplicable = productionEvidence && (profile?.project?.tenancy === 'multi-tenant-shared-schema' || /app\.workspace_id|workspace_id/.test(body))
|
|
195
|
-
if (tenancyApplicable) {
|
|
196
|
-
for (const match of body.matchAll(/\bSET\s+(?!LOCAL\b)(?:SESSION\s+)?app\.workspace_id\b/gi)) rawFinding(findings, 'TEN-003', 'tenancy.context-local', 'Tenant context must use SET LOCAL in the protected transaction', relative, body, match.index)
|
|
197
|
-
for (const match of body.matchAll(/\bBYPASSRLS\b/gi)) rawFinding(findings, 'TEN-003', 'tenancy.role-bypassrls', 'Application/request roles must not receive BYPASSRLS', relative, body, match.index)
|
|
198
|
-
}
|
|
199
|
-
if (productionEvidence && (enabled(profile, 'agents') || observed.agents.length)) for (const match of body.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[\w."-]*(?:workflow_steps|workflow_runs|agent_approvals|continuations)\b/gi)) rawFinding(findings, 'DUR-001', 'agents.dual-workflow-state', 'Do not create a second workflow, tool-loop, HITL, or continuation store', relative, body, match.index)
|
|
200
|
-
|
|
201
|
-
const specifiers = moduleSpecifiers(body)
|
|
202
|
-
if (productionEvidence && owned(profile, 'webControlPlane') && inRoots(relative, roots(profile, 'webControlPlane'))) for (const specifier of specifiers) if (specifier === 'eve' || specifier === 'pg-boss' || specifier.startsWith('@workflow/')) rawFinding(findings, specifier === 'pg-boss' ? 'RUN-002' : 'RUN-001', 'runtime.web-bundle-boundary', `Web source root must not depend on long-running runtime package ${specifier}`, relative, body, body.indexOf(specifier))
|
|
203
|
-
if (productionEvidence && owned(profile, 'agents') && inRoots(relative, roots(profile, 'agents')) && specifiers.includes('pg-boss')) rawFinding(findings, 'DUR-003', 'agents.pg-boss-ownership', 'EVE runtime must not own pg-boss orchestration', relative, body, body.indexOf('pg-boss'))
|
|
204
|
-
if (productionEvidence && owned(profile, 'jobs') && inRoots(relative, roots(profile, 'jobs')) && specifiers.some(specifier => specifier === 'eve' || specifier.startsWith('@workflow/'))) rawFinding(findings, 'DUR-001', 'jobs.workflow-ownership', 'Jobs runtime must not own EVE/Workflow state', relative, body, 0)
|
|
205
|
-
|
|
206
|
-
const authApplicable = productionEvidence && ((enabled(profile, 'webControlPlane') && profile.project?.access !== 'public') || /better-auth/.test(body))
|
|
207
|
-
if (authApplicable) {
|
|
208
|
-
for (const match of body.matchAll(/disableCSRFCheck\s*:\s*true|disableOriginCheck\s*:\s*true/g)) rawFinding(findings, 'AUTH-003', 'auth.csrf-origin', 'CSRF and origin checks must remain enabled', relative, body, match.index)
|
|
209
|
-
for (const match of body.matchAll(/trustedOrigins\s*[:=][\s\S]{0,120}(?:['"]\*|\*:\/\/|http:\/\/)/gi)) rawFinding(findings, 'AUTH-003', 'auth.trusted-origins', 'Production trusted origins must be exact HTTPS origins', relative, body, match.index)
|
|
210
|
-
for (const match of body.matchAll(/require_pkce\s*:\s*false/g)) rawFinding(findings, 'AUTH-004', 'auth.pkce', 'Delegated OAuth clients must not opt out of PKCE', relative, body, match.index)
|
|
211
|
-
for (const match of body.matchAll(/defaultSCIM\s*:\s*true|storeSCIMToken\s*:\s*['"]plain['"]/g)) rawFinding(findings, 'AUTH-006', 'identity.scim-storage', 'Production SCIM must be organization-scoped with protected token storage', relative, body, match.index)
|
|
212
|
-
for (const match of body.matchAll(/enableSessionForAPIKeys\s*:\s*true|referenceId\s*:\s*['"]user|storeKey\s*:\s*true/g)) rawFinding(findings, 'AUTH-005', 'auth.api-key-scope', 'Workspace API keys must be organization-scoped, hashed, expiring, and must not create browser sessions', relative, body, match.index)
|
|
213
|
-
}
|
|
214
|
-
if (enabled(profile, 'realtime')) for (const match of body.matchAll(/websocket/gi)) if (/default|primary/i.test(body) && /stream|run/i.test(body)) findings.push(issue('NOT_VERIFIED', 'RT-002', 'realtime.websocket-trigger', 'Verify that bidirectional presence/collaboration justifies WebSockets; default to resumable SSE', evidence(relative, body, match.index), { verificationType: 'manual-qualification', reason: 'intent cannot be proven statically', risk: 'durable state or streaming coupled to WebSocket lifetime', owner: 'project owner', nextAction: 'document and reproduce the bidirectional requirement' }))
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (skipped.symlinks.length || skipped.skillTrees.length || skipped.generated.length) {
|
|
218
|
-
const parts = []
|
|
219
|
-
if (skipped.symlinks.length) parts.push(`${skipped.symlinks.length} symlinked path(s)`)
|
|
220
|
-
if (skipped.skillTrees.length) parts.push(`${skipped.skillTrees.length} agent-skill tree(s)`)
|
|
221
|
-
if (skipped.generated.length) parts.push(`${skipped.generated.length} generated/bundled file(s)`)
|
|
222
|
-
findings.push(issue('NOT_VERIFIED', 'DEL-001', 'inspection.traversal', `Skipped without inspection: ${parts.join(', ')} (first: ${[...skipped.symlinks, ...skipped.skillTrees, ...skipped.generated][0]})`, { path: '.', line: 1, type: 'static-sentinel' }, { verificationType: 'manual-qualification', reason: 'paths were excluded from static inspection', risk: 'excluded paths may contain violations', owner: 'project owner', nextAction: 'review excluded paths or extend .guardianignore deliberately', skipped }))
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (profile) for (const [name, paths] of Object.entries(observed)) {
|
|
226
|
-
if (paths.length && !enabled(profile, name)) rawFinding(findings, 'FOUND-004', `capability.${name}.undeclared`, `Repository evidence suggests disabled capability ${name}: ${paths.slice(0, 3).join(', ')}`, paths[0])
|
|
227
|
-
if (owned(profile, name) && !content.some(item => inRoots(item.path, roots(profile, name)))) rawFinding(findings, 'FOUND-004', `capability.${name}.missing-code`, `Enabled owned capability ${name} has no inspectable code in declared source roots`, roots(profile, name)[0] ?? profileRelative)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (profile?.project?.tenancy === 'multi-tenant-shared-schema') {
|
|
231
|
-
const sqlFiles = content.filter(item => item.extension === '.sql' && !nonProductionPath(item.path))
|
|
232
|
-
const allSql = sqlFiles.map(item => item.body).join('\n')
|
|
233
|
-
const policiesByTable = tablePolicies(sqlFiles)
|
|
234
|
-
const tables = sqlFiles.flatMap(file => tenantTables(file.body).map(table => ({ ...table, path: file.path, body: file.body })))
|
|
235
|
-
for (const table of tables) {
|
|
236
|
-
const escaped = table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
237
|
-
const primary = table.definition.match(/PRIMARY\s+KEY\s*\(([^)]*)\)/i)?.[1] ?? ''
|
|
238
|
-
if (!/\bworkspace_id\b/i.test(primary)) rawFinding(findings, 'TEN-001', 'tenancy.composite-primary-key', `Tenant table ${table.name} primary key must include workspace_id`, table.path, table.body, table.index)
|
|
239
|
-
for (const match of table.definition.matchAll(/(?:UNIQUE|REFERENCES\s+[\w."-]+)\s*\(([^)]*)\)/gi)) if (!/\bworkspace_id\b/i.test(match[1])) rawFinding(findings, 'TEN-001', 'tenancy.composite-relationship', `Tenant table ${table.name} unique/foreign relationship must include workspace_id`, table.path, table.body, table.index + (match.index ?? 0))
|
|
240
|
-
if (!new RegExp(`ALTER\\s+TABLE\\s+"?${escaped}"?\\s+ENABLE\\s+ROW\\s+LEVEL\\s+SECURITY`, 'i').test(allSql)) rawFinding(findings, 'TEN-002', 'tenancy.rls-enable', `Tenant table ${table.name} must enable RLS`, table.path, table.body, table.index)
|
|
241
|
-
if (!new RegExp(`ALTER\\s+TABLE\\s+"?${escaped}"?\\s+FORCE\\s+ROW\\s+LEVEL\\s+SECURITY`, 'i').test(allSql)) rawFinding(findings, 'TEN-002', 'tenancy.rls-force', `Tenant table ${table.name} must force RLS`, table.path, table.body, table.index)
|
|
242
|
-
const policies = policiesByTable.get(normalizedSqlName(table.name)) ?? []
|
|
243
|
-
if (!policies.some(policy => tenantClause(policy.statement, 'USING')) || !policies.some(policy => tenantClause(policy.statement, 'WITH\\s+CHECK'))) rawFinding(findings, 'TEN-002', 'tenancy.rls-policy', `Tenant table ${table.name} requires table-bound fail-closed tenant USING and WITH CHECK predicates; command-specific policies may split them`, table.path, table.body, table.index)
|
|
244
|
-
}
|
|
245
|
-
if (!tables.length) rawFinding(findings, 'TEN-002', 'tenancy.rls-evidence', 'Shared-schema tenancy requires reviewed tenant table/RLS migration evidence', 'database migrations')
|
|
246
|
-
const all = content.filter(item => item.extension !== '.md' && !nonProductionPath(item.path)).map(item => item.body).join('\n')
|
|
247
|
-
if (!/(SET\s+LOCAL\s+app\.workspace_id|set_config\s*\(\s*['"]app\.workspace_id['"][^)]*,\s*true\s*\))/i.test(all)) rawFinding(findings, 'TEN-003', 'tenancy.context-evidence', 'Set tenant context locally inside each protected transaction', 'data access')
|
|
248
|
-
for (const file of sqlFiles) for (const match of file.body.matchAll(/SECURITY\s+DEFINER/gi)) {
|
|
249
|
-
const window = file.body.slice(Math.max(0, (match.index ?? 0) - 700), (match.index ?? 0) + 700)
|
|
250
|
-
if (!/SET\s+(?:LOCAL\s+)?search_path\s*(?:=|TO)\s*(?:pg_catalog|['"]?[^,;'"\s]+['"]?\s*,\s*pg_temp)/i.test(window) || !/(workspace_id|current_setting\s*\(\s*['"]app\.workspace_id|authorize)/i.test(window)) rawFinding(findings, 'TEN-004', 'tenancy.security-definer', 'SECURITY DEFINER tenant paths require a fixed search_path and explicit tenant/authorization guard', file.path, file.body, match.index)
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (profile && enabled(profile, 'webControlPlane') && profile.project.access !== 'public' && owned(profile, 'webControlPlane')) {
|
|
255
|
-
const nextCode = content.filter(item => item.extension !== '.md' && !nonProductionPath(item.path) && inRoots(item.path, roots(profile, 'webControlPlane'))).map(item => item.body).join('\n')
|
|
256
|
-
if (!/from\s+['"]better-auth['"]/.test(nextCode)) rawFinding(findings, 'AUTH-003', 'auth.library', 'Configure the reviewed Better Auth server library', 'identity configuration')
|
|
257
|
-
if (profile.project.tenancy.startsWith('multi-tenant') && !/organization\s*\(/.test(nextCode)) rawFinding(findings, 'AUTH-001', 'auth.organization-boundary', 'Configure Better Auth Organization as the workspace boundary', 'identity configuration')
|
|
258
|
-
if ((enabled(profile, 'flutter') || enabled(profile, 'connectors')) && !/oauthProvider\s*\(/.test(nextCode)) rawFinding(findings, 'AUTH-004', 'auth.oauth-provider', 'Configure Better Auth OAuth Provider for delegated clients', 'identity configuration')
|
|
259
|
-
if (!/useSecureCookies\s*:\s*true/.test(nextCode)) rawFinding(findings, 'AUTH-003', 'auth.secure-cookies-code', 'Force secure browser cookies in production', 'identity configuration')
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (profile) {
|
|
263
|
-
for (const [name, capability] of Object.entries(profile.capabilities ?? {})) if (capability.status === 'enabled' && ['shared-managed', 'external-managed'].includes(capability.ownership)) findings.push(issue('NOT_VERIFIED', 'RUN-003', `contract.${name}.qualification`, `${name} contract is declared but live compatibility, isolation, recovery and exit behavior were not reproduced`, { path: profileRelative, line: 1, type: 'declared-control' }, { capability: name, verificationType: 'manual-qualification', reason: 'environment-bound service behavior is not statically testable', risk: 'dependency contract may not meet declared boundary', owner: capability.contract?.incidentOwner, nextAction: 'run contract, failure and exit qualification' }))
|
|
264
|
-
if (!profile.objectives) findings.push(issue('NOT_VERIFIED', 'REL-001', 'reliability.objectives', 'No measured production objectives are declared', { path: profileRelative, line: 1, type: 'declared-control' }, { verificationType: 'manual-qualification', reason: 'project objectives were not provided', risk: 'capacity and recovery decisions cannot be qualified', owner: 'project owner', nextAction: 'confirm applicable SLI/SLO, RPO and RTO objectives' }))
|
|
265
|
-
if (Object.values(profile.capabilities ?? {}).some(capability => capability.status === 'enabled')) findings.push(issue('NOT_VERIFIED', 'DEL-001', 'qualification.runtime', 'Static sentinels and declarations do not prove runtime isolation, replay, failover, restore, provider security or recovery', { path: profileRelative, line: 1, type: 'static-sentinel' }, { verificationType: 'manual-qualification', reason: 'no environment-bound tests were executed by this checker', risk: 'declared controls may fail at runtime', owner: 'project owner', nextAction: 'run the scoped qualification plan for enabled capabilities' }))
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const unique = findings.filter((finding, index, all) => all.findIndex(candidate => candidate.status === finding.status && candidate.rule === finding.rule && candidate.control === finding.control && candidate.path === finding.path && candidate.evidence?.line === finding.evidence?.line && candidate.message === finding.message) === index)
|
|
269
|
-
const resolved = capFindings(applyExceptions(unique, exceptionStates, profileRelative))
|
|
270
|
-
const knownRules = await canonicalRuleIds()
|
|
271
|
-
const catalogPath = resolve(scriptDirectory, '../references/control-catalog.json')
|
|
272
|
-
const catalog = JSON.parse(await readFile(catalogPath, 'utf8'))
|
|
273
|
-
const knownControl = finding => catalog.controls.some(item => item.rule === finding.rule && (item.id === finding.control || (item.idPattern && new RegExp(item.idPattern).test(finding.control))))
|
|
274
|
-
for (const finding of resolved) {
|
|
275
|
-
if (!knownRules.has(finding.rule)) throw new Error(`Checker emitted unknown canonical rule ${finding.rule}`)
|
|
276
|
-
if (!knownControl(finding)) throw new Error(`Checker emitted unknown control ${finding.rule}/${finding.control}`)
|
|
277
|
-
}
|
|
278
|
-
if (!resolved.some(item => item.status === 'FAIL')) resolved.unshift(issue('PASS', 'FOUND-004', 'profile.capability-alignment', 'No unexcepted machine-detectable architecture violations were found', { path: profileRelative, line: 1, type: 'static-sentinel' }))
|
|
279
|
-
if (legacyProfileName && profile) console.error(`deprecation: ${profileRelative} uses the legacy .yaml name for a JSON document; rename to .vegastack/architecture.json`)
|
|
280
|
-
return resolved
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function summarize(findings) {
|
|
284
|
-
const counts = Object.fromEntries(['PASS', 'FAIL', 'EXCEPTED', 'NOT_VERIFIED'].map(status => [status, findings.filter(item => item.status === status).length]))
|
|
285
|
-
const suppressed = findings.reduce((total, item) => total + (item.suppressedCount ?? 0), 0)
|
|
286
|
-
return { status: counts.FAIL ? 'FAIL' : 'PASS', verdict: counts.FAIL || counts.EXCEPTED ? 'GUARDIAN VERDICT: REJECT' : 'GUARDIAN VERDICT: ACCEPT', counts, ...(suppressed ? { suppressedFindings: suppressed } : {}) }
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const usage = `Usage: architecture-check.mjs [DIR] [--json | --summary] [--help]
|
|
290
|
-
|
|
291
|
-
Deterministic, read-only architecture checks for the directory (default: cwd).
|
|
292
|
-
Profile discovery: .vegastack/architecture.json (preferred) or legacy .vegastack/architecture.yaml.
|
|
293
|
-
Add repository-relative path prefixes to a .guardianignore file (one per line, # comments) to exclude paths.
|
|
294
|
-
|
|
295
|
-
Output: --summary prints the verdict, counts, and the first 10 findings; --json prints everything.
|
|
296
|
-
Exit codes: 0 = no FAIL findings; 1 = FAIL findings present; 2 = usage or tool error.`
|
|
297
|
-
|
|
298
|
-
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
299
|
-
const argv = process.argv.slice(2)
|
|
300
|
-
const flags = new Set(argv.filter(argument => argument.startsWith('-')))
|
|
301
|
-
const positional = argv.filter(argument => !argument.startsWith('-'))
|
|
302
|
-
const known = new Set(['--json', '--summary', '--help', '-h'])
|
|
303
|
-
const unknown = [...flags].filter(flag => !known.has(flag))
|
|
304
|
-
if (flags.has('--help') || flags.has('-h')) { console.log(usage); process.exit(0) }
|
|
305
|
-
if (unknown.length || positional.length > 1) { console.error(`error: unknown arguments: ${[...unknown, ...positional.slice(1)].join(' ')}\n\n${usage}`); process.exit(2) }
|
|
306
|
-
const root = resolve(positional[0] ?? process.cwd())
|
|
307
|
-
let findings
|
|
308
|
-
try { findings = await checkArchitecture(root) } catch (error) { console.error(`error: ${error.message}`); process.exit(2) }
|
|
309
|
-
const summary = summarize(findings)
|
|
310
|
-
if (flags.has('--json')) console.log(JSON.stringify({ schemaVersion: 1, root, ...summary, findings }, null, 2))
|
|
311
|
-
else if (flags.has('--summary')) {
|
|
312
|
-
console.log(summary.verdict)
|
|
313
|
-
console.log(`architecture-check: ${Object.entries(summary.counts).map(([key, value]) => `${value} ${key}`).join(', ')}${summary.suppressedFindings ? `, ${summary.suppressedFindings} suppressed` : ''}`)
|
|
314
|
-
const ranked = [...findings].sort((a, b) => (a.status === 'FAIL' ? 0 : 1) - (b.status === 'FAIL' ? 0 : 1))
|
|
315
|
-
for (const finding of ranked.slice(0, 10)) console.log(`${finding.status} ${finding.rule}/${finding.control}: ${finding.message}${finding.path ? ` (${finding.path}${finding.evidence?.line ? `:${finding.evidence.line}` : ''})` : ''}`)
|
|
316
|
-
if (findings.length > 10) console.log(`(+${findings.length - 10} more findings; use --json for all)`)
|
|
317
|
-
} else {
|
|
318
|
-
console.log(summary.verdict)
|
|
319
|
-
for (const finding of findings) console.log(`${finding.status} ${finding.rule}/${finding.control}: ${finding.message}${finding.path ? ` (${finding.path}${finding.evidence?.line ? `:${finding.evidence.line}` : ''})` : ''}${finding.suppressedCount ? ` (+${finding.suppressedCount} more suppressed)` : ''}`)
|
|
320
|
-
console.log(`architecture-check: ${Object.entries(summary.counts).map(([key, value]) => `${value} ${key}`).join(', ')}`)
|
|
321
|
-
}
|
|
322
|
-
if (summary.status === 'FAIL') process.exitCode = 1
|
|
323
|
-
}
|