@thebackstoryis/engineering-with-ai 0.2.9
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/Docs/README.md +50 -0
- package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
- package/Docs/adoption/non-technical-team-guide.md +126 -0
- package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
- package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
- package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
- package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
- package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
- package/Docs/cli-reference.md +113 -0
- package/Docs/completed-phase-evidence-amendments.md +74 -0
- package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
- package/Docs/context-aware-delivery-companion-guide.md +198 -0
- package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
- package/Docs/context-management-and-token-efficiency.md +113 -0
- package/Docs/design-systems/design-system-implementation-guide.md +85 -0
- package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
- package/Docs/design-systems/design-system-review-guide.md +51 -0
- package/Docs/design-systems/design-system-user-guide.md +96 -0
- package/Docs/design-systems/product-owner-guide.md +49 -0
- package/Docs/designing-organisation-blueprint-packs.md +384 -0
- package/Docs/developer-delivery-guide.md +224 -0
- package/Docs/error-reporting-guide.md +110 -0
- package/Docs/error-reporting-provider-guide.md +49 -0
- package/Docs/examples/error-report-adapter.md +70 -0
- package/Docs/examples/meeting-review.md +76 -0
- package/Docs/examples/minimal-design-system.md +67 -0
- package/Docs/examples/prototype-review-inputs.md +175 -0
- package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
- package/Docs/examples/test-scenario-input.md +68 -0
- package/Docs/examples/worked-examples.md +147 -0
- package/Docs/existing-project-onboarding-guide.md +214 -0
- package/Docs/explanation/core-concepts.md +26 -0
- package/Docs/explanation/delivery-workflow.md +48 -0
- package/Docs/governance/governance-team-guide.md +139 -0
- package/Docs/governed-starter-project-materialisation-guide.md +284 -0
- package/Docs/guide-catalogue.md +117 -0
- package/Docs/guided-discovery-facilitator-guide.md +172 -0
- package/Docs/guided-intent-workspace-guide.md +109 -0
- package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
- package/Docs/human-approval-and-assurance-guide.md +146 -0
- package/Docs/knowledge-proposals-implementer-guide.md +106 -0
- package/Docs/knowledge-proposals-user-guide.md +247 -0
- package/Docs/maintainers/context-benchmarks.md +29 -0
- package/Docs/maintainers/contributing.md +58 -0
- package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
- package/Docs/maintainers/verification-walkthroughs.md +104 -0
- package/Docs/meeting-evidence-implementer-guide.md +132 -0
- package/Docs/meeting-evidence-user-guide.md +200 -0
- package/Docs/operations/dashboard-and-delivery-state.md +153 -0
- package/Docs/operations/dashboard-configuration.md +87 -0
- package/Docs/operations/installation-updating-and-entitlements.md +135 -0
- package/Docs/operations/premium-personas-setup.md +76 -0
- package/Docs/operations/troubleshooting-and-recovery.md +205 -0
- package/Docs/organisation-rollout-guide.md +142 -0
- package/Docs/persona-entitlement-provider-guide.md +199 -0
- package/Docs/persona-guided-prototype-iteration.md +129 -0
- package/Docs/personas/organisation-specific-personas.md +103 -0
- package/Docs/personas/persona-authoring-cookbook.md +176 -0
- package/Docs/personas/persona-engagement-ui.md +133 -0
- package/Docs/personas/persona-governance.md +118 -0
- package/Docs/platform-export-analysis-guide.md +336 -0
- package/Docs/policies/governance-owner-guide.md +36 -0
- package/Docs/policies/implementation-guide.md +42 -0
- package/Docs/policies/organisation-policy-design-gates.md +58 -0
- package/Docs/policies/policy-pack-authoring-guide.md +108 -0
- package/Docs/policies/product-owner-guide.md +43 -0
- package/Docs/policies/technical-owner-guide.md +37 -0
- package/Docs/product-owner-guide.md +327 -0
- package/Docs/project-portfolio-orchestration-guide.md +199 -0
- package/Docs/quality/manual-qa-and-acceptance.md +162 -0
- package/Docs/quality/persona-driven-test-scenarios.md +172 -0
- package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
- package/Docs/reference/capabilities-and-project-layout.md +678 -0
- package/Docs/reference/cli-and-configuration.md +398 -0
- package/Docs/reference/contributions-api.md +23 -0
- package/Docs/reference/security-adapter-authoring.md +81 -0
- package/Docs/reference/starter-adapter-authoring.md +74 -0
- package/Docs/repository-source-map-guide.md +381 -0
- package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
- package/Docs/screen-prototype-creation-guide.md +324 -0
- package/Docs/security-validation-guide.md +353 -0
- package/Docs/solution-readiness-review-guide.md +123 -0
- package/Docs/standards/project-standards-authoring.md +157 -0
- package/Docs/team-hub-guide.md +162 -0
- package/Docs/team-hub-resource-registry-guide.md +167 -0
- package/Docs/tutorials/first-delivery.md +83 -0
- package/Docs/tutorials/first-session.md +62 -0
- package/Docs/using-lifecycle-hooks.md +381 -0
- package/Docs/working-with-personas.md +274 -0
- package/LICENSE +165 -0
- package/README.md +96 -0
- package/agents-src/claude/ewai-security-reviewer.md +15 -0
- package/bin/ewai +5 -0
- package/config/archaeology-record-families.yaml +59 -0
- package/config/delivery-artifacts.yaml +121 -0
- package/config/delivery-stages.yaml +77 -0
- package/config/design-system.schema.json +46 -0
- package/config/error-reporting.schema.json +79 -0
- package/config/evidence-depth.schema.json +53 -0
- package/config/intent.schema.json +90 -0
- package/config/knowledge-proposals-proposal.schema.json +34 -0
- package/config/lifecycle-event.schema.json +68 -0
- package/config/lifecycle-handler.schema.json +45 -0
- package/config/lifecycle-hook-ack.schema.json +19 -0
- package/config/meeting-evidence-candidate.schema.json +102 -0
- package/config/organisation-policy.schema.json +137 -0
- package/config/pack.schema.json +250 -0
- package/config/persona-pack.schema.json +21 -0
- package/config/persona.schema.json +17 -0
- package/config/policy-evaluation.schema.json +77 -0
- package/config/policy-facts.schema.json +140 -0
- package/config/portfolio.schema.json +68 -0
- package/config/project.schema.json +313 -0
- package/config/prototype-iteration.schema.json +128 -0
- package/config/rollout.schema.json +87 -0
- package/config/security-adapter.schema.json +31 -0
- package/config/security-scan-request.schema.json +64 -0
- package/config/security-scan-response.schema.json +52 -0
- package/config/security-validation-policy.schema.json +74 -0
- package/config/starter-source-acknowledgement.schema.json +13 -0
- package/config/starter-source-adapter.schema.json +38 -0
- package/config/starter-source-request.schema.json +61 -0
- package/package.json +77 -0
- package/packs/core/pack.yaml +7 -0
- package/packs/design-systems/default/experience-promise.md +9 -0
- package/packs/design-systems/default/intentional-review.md +10 -0
- package/packs/design-systems/default/interaction-and-entry.md +9 -0
- package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
- package/packs/design-systems/default/pack.yaml +44 -0
- package/packs/design-systems/default/principles.md +10 -0
- package/packs/personas/core/pack.yaml +7 -0
- package/packs/personas/core/personas/archaeologist.md +37 -0
- package/packs/personas/core/personas/end-user.md +17 -0
- package/packs/personas/core/personas/maintainer.md +17 -0
- package/packs/personas/core/personas/operator.md +17 -0
- package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
- package/packs/technologies/laravel/pack.yaml +30 -0
- package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
- package/packs/technologies/nuxt/pack.yaml +30 -0
- package/packs/technologies/power-platform/pack.yaml +31 -0
- package/packs/technologies/salesforce/pack.yaml +25 -0
- package/public/app.js +4896 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/backstory-icon.png +0 -0
- package/public/dashboard-navigation.js +98 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +789 -0
- package/public/styles.css +2693 -0
- package/public/team-hub/app.js +202 -0
- package/public/team-hub/index.html +79 -0
- package/public/team-hub/styles.css +90 -0
- package/scripts/publication-check.mjs +140 -0
- package/scripts/setup.mjs +21 -0
- package/skills-src/ewai-archaeology/SKILL.md +334 -0
- package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
- package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
- package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
- package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
- package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
- package/skills-src/ewai-architecture/SKILL.md +108 -0
- package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
- package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
- package/skills-src/ewai-context/SKILL.md +68 -0
- package/skills-src/ewai-context/agents/openai.yaml +4 -0
- package/skills-src/ewai-context-import/SKILL.md +118 -0
- package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
- package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
- package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
- package/skills-src/ewai-deliver/SKILL.md +122 -0
- package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
- package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
- package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
- package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
- package/skills-src/ewai-design-system-author/SKILL.md +28 -0
- package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
- package/skills-src/ewai-design-system-review/SKILL.md +26 -0
- package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
- package/skills-src/ewai-error-reporting/SKILL.md +46 -0
- package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
- package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
- package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
- package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
- package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
- package/skills-src/ewai-intent/SKILL.md +68 -0
- package/skills-src/ewai-intent/agents/openai.yaml +4 -0
- package/skills-src/ewai-intent/references/intent-contract.md +42 -0
- package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
- package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
- package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
- package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
- package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
- package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
- package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
- package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
- package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
- package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
- package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
- package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
- package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
- package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
- package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
- package/skills-src/ewai-pipeline/SKILL.md +130 -0
- package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
- package/skills-src/ewai-pipeline/references/cli.md +86 -0
- package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
- package/skills-src/ewai-portfolio/SKILL.md +70 -0
- package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
- package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
- package/skills-src/ewai-project-discovery/SKILL.md +95 -0
- package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
- package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
- package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
- package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
- package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
- package/skills-src/ewai-retro/SKILL.md +48 -0
- package/skills-src/ewai-retro/agents/openai.yaml +4 -0
- package/skills-src/ewai-retro/references/asset-routing.md +14 -0
- package/skills-src/ewai-rollout/SKILL.md +74 -0
- package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
- package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
- package/skills-src/ewai-shape-intents/SKILL.md +84 -0
- package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
- package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
- package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
- package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
- package/skills-src/ewai-standards-check/SKILL.md +93 -0
- package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
- package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
- package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
- package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
- package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
- package/src/afk-worker.mjs +16 -0
- package/src/archaeology.mjs +1333 -0
- package/src/checkin.mjs +261 -0
- package/src/cli.mjs +2427 -0
- package/src/companion-guidance.mjs +257 -0
- package/src/companion-opening.mjs +62 -0
- package/src/companion.mjs +256 -0
- package/src/context.mjs +210 -0
- package/src/dashboard-preferences.mjs +80 -0
- package/src/delivery-artifacts.mjs +204 -0
- package/src/delivery-documents.mjs +248 -0
- package/src/delivery-gates.mjs +317 -0
- package/src/delivery.mjs +1433 -0
- package/src/design-system-application.mjs +291 -0
- package/src/design-system-authoring.mjs +101 -0
- package/src/design-systems.mjs +466 -0
- package/src/discovery.mjs +1314 -0
- package/src/error-reporting.mjs +323 -0
- package/src/evidence-depth.mjs +543 -0
- package/src/execution-state.mjs +243 -0
- package/src/install.mjs +166 -0
- package/src/intent-dependencies.mjs +117 -0
- package/src/intent-maps.mjs +402 -0
- package/src/intents.mjs +747 -0
- package/src/knowledge-proposals.mjs +717 -0
- package/src/launcher.mjs +51 -0
- package/src/meeting-evidence.mjs +703 -0
- package/src/network-rollout.mjs +386 -0
- package/src/organisation-blueprints.mjs +438 -0
- package/src/organisation-policies.mjs +448 -0
- package/src/packs.mjs +44 -0
- package/src/paths.mjs +62 -0
- package/src/persona-entitlements.mjs +438 -0
- package/src/persona-licence-config.mjs +98 -0
- package/src/persona-website-provider.mjs +134 -0
- package/src/persona-zip.mjs +87 -0
- package/src/personas.mjs +159 -0
- package/src/platform-metadata-analysis.mjs +314 -0
- package/src/policy-design-gates.mjs +623 -0
- package/src/policy-gate-integration.mjs +318 -0
- package/src/portfolio.mjs +509 -0
- package/src/power-platform-source-map.mjs +190 -0
- package/src/project.mjs +449 -0
- package/src/prototype-iterations.mjs +730 -0
- package/src/repository-source-map.mjs +603 -0
- package/src/runtime/afk-conductor.mjs +973 -0
- package/src/runtime/context-assembly.mjs +457 -0
- package/src/runtime/context-benchmarks.mjs +115 -0
- package/src/runtime/dashboard-actions.mjs +109 -0
- package/src/runtime/dashboard-handoffs.mjs +149 -0
- package/src/runtime/dashboard-server.mjs +1272 -0
- package/src/runtime/dashboard.mjs +197 -0
- package/src/runtime/database.mjs +789 -0
- package/src/runtime/error-reporting.mjs +581 -0
- package/src/runtime/evidence-depth-workspace.mjs +412 -0
- package/src/runtime/execution-leases.mjs +299 -0
- package/src/runtime/guided-discovery.mjs +350 -0
- package/src/runtime/guided-intents.mjs +517 -0
- package/src/runtime/impact-analysis.mjs +535 -0
- package/src/runtime/intents.mjs +222 -0
- package/src/runtime/knowledge.mjs +86 -0
- package/src/runtime/lifecycle-hooks.mjs +1239 -0
- package/src/runtime/mcp-config.mjs +110 -0
- package/src/runtime/mcp-server.mjs +1885 -0
- package/src/runtime/palace.mjs +362 -0
- package/src/runtime/paths.mjs +58 -0
- package/src/runtime/persona-engagement.mjs +255 -0
- package/src/runtime/phase-contributions.mjs +594 -0
- package/src/runtime/policy-workspace.mjs +170 -0
- package/src/runtime/prototype-iterations.mjs +235 -0
- package/src/runtime/provider-adapters.mjs +163 -0
- package/src/runtime/repository-index.mjs +838 -0
- package/src/runtime/runs.mjs +185 -0
- package/src/runtime/security-validation.mjs +1230 -0
- package/src/runtime/starter-materialisation.mjs +1155 -0
- package/src/runtime/team-hub-client.mjs +479 -0
- package/src/runtime/team-hub-database.mjs +288 -0
- package/src/runtime/team-hub-server.mjs +191 -0
- package/src/runtime/team-hub.mjs +110 -0
- package/src/runtime/tree-sitter-index.mjs +390 -0
- package/src/runtime/version.mjs +1 -0
- package/src/runtime/work.mjs +633 -0
- package/src/salesforce-source-map.mjs +212 -0
- package/src/security-validation-config.mjs +224 -0
- package/src/solution-readiness.mjs +620 -0
- package/src/starter-materialisation-contract.mjs +407 -0
- package/src/task-graph.mjs +544 -0
- package/src/team-hub-resources.mjs +239 -0
- package/src/team-hub.mjs +242 -0
- package/src/test-scenarios.mjs +622 -0
- package/src/validation-config.mjs +289 -0
- package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
- package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
- package/templates/SPECS/6.Build/_tracker-template.md +16 -0
- package/templates/SPECS/pipeline.yaml +62 -0
- package/templates/discovery-answers.yaml +86 -0
- package/templates/intent-body.md +21 -0
- package/tests/fixtures/context-benchmarks.json +9 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve } from 'node:path';
|
|
16
|
+
import YAML from 'yaml';
|
|
17
|
+
import { readPersonaLicence, guardPersonaPath, withPersonaMutationLock, personaAccessDenialPath, readPersonaAccessDenial, writePrivatePersonaJson } from './persona-licence-config.mjs';
|
|
18
|
+
import { DEFAULT_PERSONA_SERVER, personaServer, websitePersonaStatus, acquireWebsitePersonaArchive } from './persona-website-provider.mjs';
|
|
19
|
+
import { extractPersonaZip } from './persona-zip.mjs';
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_PREMIUM_UPGRADE_URL = 'https://www.conversationalcoding.dev/personas/';
|
|
22
|
+
export const PREMIUM_PACK_ID = 'ewai.personas.professional';
|
|
23
|
+
export const PERSONA_ENTITLEMENT_SCHEMA = 'ewai.persona-entitlement/v1';
|
|
24
|
+
export const PERSONA_PACK_RECEIPT_SCHEMA = 'ewai.persona-pack-receipt/v1';
|
|
25
|
+
|
|
26
|
+
export const PERSONA_PACK_LIMITS = Object.freeze({
|
|
27
|
+
maxFiles: 1000,
|
|
28
|
+
maxFileBytes: 1024 * 1024,
|
|
29
|
+
maxTotalBytes: 16 * 1024 * 1024,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const allowedExtensions = new Set(['.csv', '.json', '.md', '.markdown', '.txt', '.yaml', '.yml']);
|
|
33
|
+
const allowedExtensionless = new Set(['license', 'notice', 'readme']);
|
|
34
|
+
const revisionPattern = /^[a-f0-9]{64}$/;
|
|
35
|
+
const semverPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
36
|
+
|
|
37
|
+
function clean(value) {
|
|
38
|
+
return String(value ?? '').trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isWithin(root, candidate) {
|
|
42
|
+
const rel = relative(resolve(root), resolve(candidate));
|
|
43
|
+
return rel === '' || Boolean(rel && !rel.startsWith('..') && !isAbsolute(rel));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function safeRelativePath(value, label) {
|
|
47
|
+
const path = clean(value).replaceAll('\\', '/');
|
|
48
|
+
if (!path || path.startsWith('/') || path.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
49
|
+
throw new Error(`${label} must be a safe relative path`);
|
|
50
|
+
}
|
|
51
|
+
return path;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolvePersonaEntitlement(config = {}, options = {}) {
|
|
55
|
+
const configured = config.personas?.premium ?? config.premium ?? config;
|
|
56
|
+
if (configured.provider && configured.provider !== 'wordpress-edd'
|
|
57
|
+
|| ['repository', 'branch', 'upgrade_url', 'upgradeUrl'].some(key => Object.hasOwn(configured, key))) {
|
|
58
|
+
throw new Error('Premium personas use website licences only. Remove obsolete persona source fields and run ewai persona premium configure --project .');
|
|
59
|
+
}
|
|
60
|
+
const saved = readPersonaLicence(options.home ?? homedir());
|
|
61
|
+
return {
|
|
62
|
+
id: PREMIUM_PACK_ID, provider: 'wordpress-edd',
|
|
63
|
+
server: personaServer(configured.server || saved?.server || DEFAULT_PERSONA_SERVER),
|
|
64
|
+
upgradeUrl: DEFAULT_PREMIUM_UPGRADE_URL,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function premiumPackRoot(home = homedir()) {
|
|
69
|
+
return resolve(home, '.ewai/packs', PREMIUM_PACK_ID);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function premiumPersonaRoot(home = homedir()) {
|
|
73
|
+
try {
|
|
74
|
+
const receipt = readReceipt(home);
|
|
75
|
+
if (!receipt || receiptDeniesAccess(receipt, home)) return '';
|
|
76
|
+
} catch {
|
|
77
|
+
// An unsafe receipt is not a route into content via another persona caller.
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
return resolve(premiumPackRoot(home), 'premium-personas');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function premiumPackReceiptPath(home = homedir()) {
|
|
84
|
+
return resolve(home, '.ewai/packs/.receipts', `${PREMIUM_PACK_ID}.json`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseManifest(root, expectedPackId) {
|
|
88
|
+
const path = resolve(root, 'pack.yaml');
|
|
89
|
+
if (!existsSync(path) || !lstatSync(path).isFile()) throw new Error('Persona pack is missing pack.yaml');
|
|
90
|
+
if (lstatSync(path).size > PERSONA_PACK_LIMITS.maxFileBytes) throw new Error('Persona pack manifest exceeds the maximum size');
|
|
91
|
+
let manifest;
|
|
92
|
+
try {
|
|
93
|
+
manifest = YAML.parse(readFileSync(path, 'utf8'), { maxAliasCount: 0 });
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error('Persona pack manifest is not valid YAML');
|
|
96
|
+
}
|
|
97
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
98
|
+
throw new Error('Persona pack manifest must be an object');
|
|
99
|
+
}
|
|
100
|
+
const personaPath = safeRelativePath(manifest.content?.personas, 'Persona pack content.personas');
|
|
101
|
+
if (manifest.schema === undefined) {
|
|
102
|
+
return { raw: manifest, packId: expectedPackId, manifestVersion: null, personaPath, compatibility: 'compatible-legacy' };
|
|
103
|
+
}
|
|
104
|
+
const richPersonaPack = manifest.schema === 'ewai.pack/v1' && manifest.type === 'persona';
|
|
105
|
+
if (manifest.schema !== 'ewai.persona-pack/v1' && !richPersonaPack) throw new Error('Persona pack schema is unsupported');
|
|
106
|
+
if (clean(manifest.id) !== expectedPackId) throw new Error(`Persona pack id must be ${expectedPackId}`);
|
|
107
|
+
if (!semverPattern.test(clean(manifest.version))) throw new Error('Persona pack version must be semantic version text');
|
|
108
|
+
const allowed = new Set(['schema', 'id', 'version', 'content']);
|
|
109
|
+
if (richPersonaPack) {
|
|
110
|
+
// Source-library metadata is inert. Never allow commands, skills, deployment
|
|
111
|
+
// or nonempty boilerplates through this persona-only delivery boundary.
|
|
112
|
+
for (const key of ['name', 'type', 'description', 'commercial', 'source', 'updates', 'compatibility', 'normalisation_status', 'boilerplates']) allowed.add(key);
|
|
113
|
+
if (typeof manifest.name !== 'string' || !manifest.name.trim()
|
|
114
|
+
|| manifest.boilerplates !== undefined && (!Array.isArray(manifest.boilerplates) || manifest.boilerplates.length)) throw new Error('Persona source pack contains unsupported material');
|
|
115
|
+
}
|
|
116
|
+
if (Object.keys(manifest).some((key) => !allowed.has(key))) throw new Error('Persona pack manifest contains unsupported fields');
|
|
117
|
+
if (!manifest.content || typeof manifest.content !== 'object' || Array.isArray(manifest.content)
|
|
118
|
+
|| Object.keys(manifest.content).some((key) => key !== 'personas')) {
|
|
119
|
+
throw new Error('Persona pack content must contain only personas');
|
|
120
|
+
}
|
|
121
|
+
return { raw: manifest, packId: expectedPackId, manifestVersion: clean(manifest.version), personaPath, compatibility: 'current' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function allowedFile(path) {
|
|
125
|
+
const extension = extname(path).toLowerCase();
|
|
126
|
+
if (extension) return allowedExtensions.has(extension);
|
|
127
|
+
return allowedExtensionless.has(basename(path).toLowerCase());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function collectPackFiles(root, limits) {
|
|
131
|
+
const files = [];
|
|
132
|
+
let totalBytes = 0;
|
|
133
|
+
function visit(folder, relativeFolder = '') {
|
|
134
|
+
for (const name of readdirSync(folder).sort((left, right) => left.localeCompare(right))) {
|
|
135
|
+
if (name.startsWith('.')) throw new Error('Persona packs may not contain hidden metadata');
|
|
136
|
+
const absolute = resolve(folder, name);
|
|
137
|
+
if (!isWithin(root, absolute)) throw new Error('Persona pack path escapes its root');
|
|
138
|
+
const relativePath = relativeFolder ? `${relativeFolder}/${name}` : name;
|
|
139
|
+
const stat = lstatSync(absolute);
|
|
140
|
+
if (stat.isSymbolicLink()) throw new Error(`Persona packs may not contain symbolic links: ${relativePath}`);
|
|
141
|
+
if (stat.isDirectory()) {
|
|
142
|
+
visit(absolute, relativePath);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!stat.isFile()) throw new Error(`Persona packs may contain regular files only: ${relativePath}`);
|
|
146
|
+
if (!allowedFile(relativePath)) throw new Error(`Persona pack file type is not allowed: ${relativePath}`);
|
|
147
|
+
if (stat.size > limits.maxFileBytes) throw new Error(`Persona pack file exceeds the maximum size: ${relativePath}`);
|
|
148
|
+
totalBytes += stat.size;
|
|
149
|
+
if (totalBytes > limits.maxTotalBytes) throw new Error('Persona pack exceeds the maximum total size');
|
|
150
|
+
files.push({ absolute, relativePath, size: stat.size });
|
|
151
|
+
if (files.length > limits.maxFiles) throw new Error('Persona pack exceeds the maximum file count');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
visit(root);
|
|
155
|
+
return { files, totalBytes };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function contentDigest(files) {
|
|
159
|
+
const hash = createHash('sha256');
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
hash.update(file.relativePath, 'utf8');
|
|
162
|
+
hash.update('\0');
|
|
163
|
+
hash.update(String(file.size), 'utf8');
|
|
164
|
+
hash.update('\0');
|
|
165
|
+
hash.update(readFileSync(file.absolute));
|
|
166
|
+
hash.update('\0');
|
|
167
|
+
}
|
|
168
|
+
return `sha256:${hash.digest('hex')}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function validatePersonaPack(root, options = {}) {
|
|
172
|
+
const candidate = resolve(root);
|
|
173
|
+
if (!existsSync(candidate) || !lstatSync(candidate).isDirectory()) throw new Error('Persona pack candidate is not a directory');
|
|
174
|
+
const packId = clean(options.packId || PREMIUM_PACK_ID);
|
|
175
|
+
const providerId = clean(options.providerId || 'wordpress-edd');
|
|
176
|
+
if (providerId !== 'wordpress-edd') throw new Error('Persona pack provider must use website licences');
|
|
177
|
+
const revision = clean(options.revision);
|
|
178
|
+
if (!revisionPattern.test(revision)) throw new Error('Persona pack revision is invalid');
|
|
179
|
+
const manifest = parseManifest(candidate, packId);
|
|
180
|
+
const personaRoot = resolve(candidate, manifest.personaPath);
|
|
181
|
+
if (!isWithin(candidate, personaRoot) || !existsSync(personaRoot) || !lstatSync(personaRoot).isDirectory()) {
|
|
182
|
+
throw new Error('Persona pack is missing its declared persona directory');
|
|
183
|
+
}
|
|
184
|
+
const collected = collectPackFiles(candidate, { ...PERSONA_PACK_LIMITS, ...(options.limits ?? {}) });
|
|
185
|
+
const personaFiles = collected.files.filter((file) => file.relativePath.startsWith(`${manifest.personaPath}/`));
|
|
186
|
+
if (!personaFiles.length) throw new Error('Persona pack declared persona directory is empty');
|
|
187
|
+
return {
|
|
188
|
+
packId,
|
|
189
|
+
providerId,
|
|
190
|
+
revision,
|
|
191
|
+
manifestVersion: manifest.manifestVersion,
|
|
192
|
+
compatibility: manifest.compatibility,
|
|
193
|
+
personaPath: manifest.personaPath,
|
|
194
|
+
contentDigest: contentDigest(collected.files),
|
|
195
|
+
fileCount: collected.files.length,
|
|
196
|
+
totalBytes: collected.totalBytes,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function readReceipt(home) {
|
|
201
|
+
const path = premiumPackReceiptPath(home);
|
|
202
|
+
guardPersonaPath(home, path);
|
|
203
|
+
if (!existsSync(path)) return null;
|
|
204
|
+
try {
|
|
205
|
+
const receipt = JSON.parse(readFileSync(path, 'utf8'));
|
|
206
|
+
if (receipt.schema !== PERSONA_PACK_RECEIPT_SCHEMA
|
|
207
|
+
|| receipt.packId !== PREMIUM_PACK_ID
|
|
208
|
+
|| receipt.providerId !== 'wordpress-edd'
|
|
209
|
+
|| !revisionPattern.test(clean(receipt.revision))
|
|
210
|
+
|| !/^sha256:[a-f0-9]{64}$/.test(clean(receipt.contentDigest))) return null;
|
|
211
|
+
return receipt;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function atomicReceipt(home, receipt) {
|
|
218
|
+
const path = premiumPackReceiptPath(home);
|
|
219
|
+
guardPersonaPath(home, path);
|
|
220
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
221
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
222
|
+
writeFileSync(temporary, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
223
|
+
chmodSync(temporary, 0o600);
|
|
224
|
+
renameSync(temporary, path);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function inspectPersonaEntitlement(config = {}, options = {}) {
|
|
228
|
+
let configuration;
|
|
229
|
+
try { configuration = resolvePersonaEntitlement(config, options); }
|
|
230
|
+
catch (error) {
|
|
231
|
+
const configured = config.personas?.premium ?? config.premium ?? config;
|
|
232
|
+
if (configured.provider && configured.provider !== 'wordpress-edd'
|
|
233
|
+
|| ['repository', 'branch', 'upgrade_url', 'upgradeUrl'].some(key => Object.hasOwn(configured, key))) throw error;
|
|
234
|
+
return websiteSafeStatus({ id: PREMIUM_PACK_ID, provider: 'wordpress-edd' }, { installed: existsSync(premiumPackRoot(options.home ?? homedir())) }, null, 'private-config-unavailable');
|
|
235
|
+
}
|
|
236
|
+
return inspectWebsitePack(configuration, options);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function receiptFor(verified, now = new Date().toISOString()) {
|
|
240
|
+
return {
|
|
241
|
+
schema: PERSONA_PACK_RECEIPT_SCHEMA,
|
|
242
|
+
packId: verified.packId,
|
|
243
|
+
providerId: verified.providerId,
|
|
244
|
+
manifestVersion: verified.manifestVersion,
|
|
245
|
+
revision: verified.revision,
|
|
246
|
+
contentDigest: verified.contentDigest,
|
|
247
|
+
compatibility: verified.compatibility,
|
|
248
|
+
verifiedAt: now,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function syncPersonaPack(config = {}, options = {}) {
|
|
253
|
+
if (options.confirmed !== true) throw new Error('Premium persona downloads require explicit confirmation; pass --yes after the user agrees');
|
|
254
|
+
return syncWebsitePack(resolvePersonaEntitlement(config, options), options);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function readPremiumPackManifest(home = homedir()) {
|
|
258
|
+
const path = resolve(premiumPackRoot(home), 'pack.yaml');
|
|
259
|
+
return existsSync(path) ? YAML.parse(readFileSync(path, 'utf8'), { maxAliasCount: 0 }) : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function websiteLocal(home) {
|
|
263
|
+
if (!existsSync(home)) return { installed: false, verified: false, receipt: null, validation: null, dirty: false };
|
|
264
|
+
const root = guardPersonaPath(home, premiumPackRoot(home));
|
|
265
|
+
if (!existsSync(root)) return { installed: false, verified: false, receipt: null, validation: null, dirty: false };
|
|
266
|
+
const receipt = readReceipt(home);
|
|
267
|
+
let validation = null;
|
|
268
|
+
if (receipt?.providerId === 'wordpress-edd' && lstatSync(root).isDirectory() ) {
|
|
269
|
+
try { validation = validatePersonaPack(root, { providerId: 'wordpress-edd', revision: receipt.revision }); } catch {}
|
|
270
|
+
}
|
|
271
|
+
const verified = Boolean(validation && validation.contentDigest === receipt.contentDigest
|
|
272
|
+
&& validation.manifestVersion === receipt.manifestVersion && validation.compatibility === receipt.compatibility
|
|
273
|
+
&& receipt.provenance?.server && receipt.provenance?.seatId && receipt.provenance?.subscriptionId
|
|
274
|
+
&& ['team', 'individual'].includes(receipt.provenance.planType));
|
|
275
|
+
return { installed: true, receipt, validation, verified, dirty: !verified, accessBlocked: receiptDeniesAccess(receipt, home) };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function receiptDeniesAccess(receipt, home) {
|
|
279
|
+
if (receipt?.providerId !== 'wordpress-edd' || receipt.provenance?.planType !== 'team') return false;
|
|
280
|
+
if (receipt.accessBlocked === 'expired-team') return true;
|
|
281
|
+
try {
|
|
282
|
+
const denied = readPersonaAccessDenial(home);
|
|
283
|
+
return denied?.schema === 'ewai.persona-access-denial/v1' && denied.installationId === receipt.installationId
|
|
284
|
+
&& denied.server === receipt.provenance.server && denied.seatId === receipt.provenance.seatId
|
|
285
|
+
&& denied.subscriptionId === receipt.provenance.subscriptionId;
|
|
286
|
+
} catch { return true; }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function receiptMatches(receipt, remote) {
|
|
290
|
+
const p = receipt?.provenance;
|
|
291
|
+
return receipt?.providerId === 'wordpress-edd' && p?.server === remote?.server
|
|
292
|
+
&& p?.seatId === remote?.seatId && p?.subscriptionId === remote?.subscription.id
|
|
293
|
+
&& p?.planType === remote?.subscription.planType;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function websiteSafeStatus(configuration, local, remote, reason = null) {
|
|
297
|
+
const access = remote?.access ?? 'unknown';
|
|
298
|
+
const expired = remote?.subscription.status === 'expired';
|
|
299
|
+
const matches = receiptMatches(local.receipt, remote);
|
|
300
|
+
const accessBlocked = local.accessBlocked || local.receipt?.accessBlocked === 'expired-team';
|
|
301
|
+
const status = accessBlocked && access !== 'available' ? 'expired-team-cleanup-blocked'
|
|
302
|
+
: accessBlocked ? 'installed-unverified'
|
|
303
|
+
: expired && matches && remote.subscription.planType === 'individual'
|
|
304
|
+
? 'expired-individual-retained'
|
|
305
|
+
: !local.installed ? 'not-installed' : !local.verified ? 'installed-unverified'
|
|
306
|
+
: access !== 'available' ? 'installed-not-remotely-verified'
|
|
307
|
+
: matches && local.receipt.revision === remote.release.sha256 && local.receipt.manifestVersion === remote.release.version ? 'current' : 'update-available';
|
|
308
|
+
let action = null;
|
|
309
|
+
if (reason === 'licence-not-configured') action = { kind: 'offer-configure', prompt: 'Have a persona licence? Configure it using the private terminal prompt.', command: 'ewai persona premium configure --project .' };
|
|
310
|
+
else if (access === 'available') {
|
|
311
|
+
const command = 'ewai persona premium sync --project . --yes';
|
|
312
|
+
if (local.dirty) action = { kind: 'blocked-local-changes', prompt: 'The managed premium pack has changed locally. Resolve those changes before replacing it.', command: null };
|
|
313
|
+
else if (!local.installed) action = { kind: 'offer-install', prompt: 'Install your licensed premium personas?', command };
|
|
314
|
+
else if (!matches) action = { kind: 'offer-replace', prompt: 'This pack belongs to another provider or licence. Explicitly replace it with this subscription?', command: command + ' --replace' };
|
|
315
|
+
else if (!local.verified || accessBlocked) action = { kind: 'offer-repair', prompt: 'Replace the unverified or previously expired pack with a verified download?', command };
|
|
316
|
+
else if (status === 'update-available') action = { kind: 'offer-update', prompt: 'A new persona release is available. Download the update?', command };
|
|
317
|
+
}
|
|
318
|
+
const message = accessBlocked
|
|
319
|
+
? access === 'available' ? 'Your subscription is available again. Install a verified pack to resume using its premium personas.'
|
|
320
|
+
: 'This team pack has a confirmed expiry. Its modified files were preserved for investigation, but it is excluded from premium persona selection.'
|
|
321
|
+
: status === 'expired-individual-retained'
|
|
322
|
+
? 'Your annual access has expired. Keep using your installed individual personas; you are no longer receiving updates.'
|
|
323
|
+
: expired ? 'This team subscription has expired. Access to its premium personas has ended.'
|
|
324
|
+
: reason === 'licence-not-configured' ? 'Configure your licence to use website-purchased personas. Core EWAI remains available.'
|
|
325
|
+
: reason ? 'Persona access could not be verified. Check your connection or configure the licence again. Installed content has not been removed.'
|
|
326
|
+
: null;
|
|
327
|
+
return {
|
|
328
|
+
schema: PERSONA_ENTITLEMENT_SCHEMA, packId: PREMIUM_PACK_ID, provider: 'wordpress-edd',
|
|
329
|
+
access, accessReason: reason || (access === 'unavailable' ? 'subscription-' + remote.subscription.status : null),
|
|
330
|
+
status, installed: Boolean(local.installed), verified: Boolean(local.verified && !accessBlocked), dirty: Boolean(local.dirty),
|
|
331
|
+
compatibility: local.validation?.compatibility ?? null, manifestVersion: local.validation?.manifestVersion ?? null,
|
|
332
|
+
version: local.receipt?.manifestVersion ?? null, latestVersion: remote?.release?.version ?? null,
|
|
333
|
+
revision: local.verified ? local.receipt.revision : null, latestRevision: remote?.release?.sha256 ?? null,
|
|
334
|
+
contentDigest: local.verified ? local.validation.contentDigest : null,
|
|
335
|
+
planType: remote?.subscription.planType ?? null, subscriptionStatus: remote?.subscription.status ?? null,
|
|
336
|
+
updatesAvailable: access === 'available' && status === 'update-available',
|
|
337
|
+
upgradeUrl: (access === 'unavailable' || reason === 'licence-not-configured') && !local.installed ? configuration.upgradeUrl : null,
|
|
338
|
+
message, action,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function inspectWebsitePack(configuration, options) {
|
|
343
|
+
const home = options.home ?? homedir();
|
|
344
|
+
let local;
|
|
345
|
+
try { local = await websiteLocal(home); }
|
|
346
|
+
catch { return websiteSafeStatus(configuration, { installed: existsSync(premiumPackRoot(home)) }, null, 'unsafe-local-state'); }
|
|
347
|
+
let remote;
|
|
348
|
+
try {
|
|
349
|
+
const credentials = readPersonaLicence(home);
|
|
350
|
+
if (!credentials) return websiteSafeStatus(configuration, local, null, 'licence-not-configured');
|
|
351
|
+
if (personaServer(credentials.server) !== configuration.server) throw new Error('server mismatch');
|
|
352
|
+
remote = await websitePersonaStatus(credentials, options);
|
|
353
|
+
} catch (e) { return websiteSafeStatus(configuration, local, null, e.code === 'invalid_activation' ? 'invalid-activation' : 'provider-access-unknown'); }
|
|
354
|
+
if (remote.subscription.status === 'expired' && remote.subscription.planType === 'team'
|
|
355
|
+
&& local.installed && receiptMatches(local.receipt, remote)) {
|
|
356
|
+
try {
|
|
357
|
+
// Independent access projection is generation-bound: a busy mutation lock
|
|
358
|
+
// must not make a confirmed expired pack eligible for persona selection.
|
|
359
|
+
writePrivatePersonaJson(home, personaAccessDenialPath(home), {
|
|
360
|
+
schema: 'ewai.persona-access-denial/v1', installationId: local.receipt.installationId,
|
|
361
|
+
server: remote.server, seatId: remote.seatId, subscriptionId: remote.subscription.id,
|
|
362
|
+
});
|
|
363
|
+
await withPersonaMutationLock(home, async () => {
|
|
364
|
+
const current = await websiteLocal(home);
|
|
365
|
+
if (current.receipt?.installationId !== local.receipt.installationId) {
|
|
366
|
+
const error = new Error('The installed pack changed during the expiry check');
|
|
367
|
+
error.code = 'local-generation-changed'; throw error;
|
|
368
|
+
}
|
|
369
|
+
if (!receiptMatches(current.receipt, remote)) throw new Error('provenance changed');
|
|
370
|
+
// Persist the known expiry outside the pack before any deletion.
|
|
371
|
+
// Every premium catalogue caller honours it, even after a later outage.
|
|
372
|
+
atomicReceipt(home, { ...current.receipt, accessBlocked: 'expired-team' });
|
|
373
|
+
if (current.dirty || !current.verified) throw new Error('local changes block cleanup');
|
|
374
|
+
const root = guardPersonaPath(home, premiumPackRoot(home));
|
|
375
|
+
const receiptPath = guardPersonaPath(home, premiumPackReceiptPath(home));
|
|
376
|
+
rmSync(root, { recursive: true }); rmSync(receiptPath, { force: true });
|
|
377
|
+
});
|
|
378
|
+
return { ...websiteSafeStatus(configuration, { installed: false, verified: false }, remote),
|
|
379
|
+
status: 'expired-team-removed', message: 'This team subscription has expired. Its managed premium personas were removed; your own personas are unchanged.' };
|
|
380
|
+
} catch (e) {
|
|
381
|
+
if (e.code === 'local-generation-changed') return websiteSafeStatus(configuration, await websiteLocal(home), null, 'local-generation-changed');
|
|
382
|
+
return { ...websiteSafeStatus(configuration, local, remote), status: 'expired-team-cleanup-blocked', verified: false,
|
|
383
|
+
message: 'This team subscription has expired, but safe cleanup could not finish. Resolve the cache issue before using its premium personas.' }; }
|
|
384
|
+
}
|
|
385
|
+
return websiteSafeStatus(configuration, local, remote);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function syncWebsitePack(configuration, options) {
|
|
389
|
+
const home = options.home ?? homedir();
|
|
390
|
+
const credentials = readPersonaLicence(home);
|
|
391
|
+
if (!credentials) throw new Error('Configure your persona licence first: ewai persona premium configure --project .');
|
|
392
|
+
if (personaServer(credentials.server) !== configuration.server) throw new Error('Configured persona server differs from the stored licence; configure it again');
|
|
393
|
+
const remote = await websitePersonaStatus(credentials, options);
|
|
394
|
+
if (remote.access !== 'available') throw new Error(remote.subscription.status === 'expired' ? 'This subscription has expired; persona updates have ended' : 'This subscription is not currently available');
|
|
395
|
+
return withPersonaMutationLock(home, async () => {
|
|
396
|
+
const local = await websiteLocal(home), root = premiumPackRoot(home);
|
|
397
|
+
if (local.dirty) throw new Error('Premium cache has local changes; it was not overwritten');
|
|
398
|
+
if (local.installed && !receiptMatches(local.receipt, remote) && options.replaceConfirmed !== true) throw new Error('This pack belongs to another provider or licence. Confirm replacement separately with --replace');
|
|
399
|
+
if (local.verified && !local.accessBlocked && !local.receipt.accessBlocked && receiptMatches(local.receipt, remote) && local.receipt.revision === remote.release.sha256
|
|
400
|
+
&& local.receipt.manifestVersion === remote.release.version && options.forceRepair !== true) {
|
|
401
|
+
return { schema: 'ewai.premium-sync/v1', status: 'current', provider: 'wordpress-edd', version: local.receipt.manifestVersion };
|
|
402
|
+
}
|
|
403
|
+
const stagingRoot = guardPersonaPath(home, resolve(dirname(root), '.staging'));
|
|
404
|
+
mkdirSync(stagingRoot, { recursive: true, mode: 0o700 });
|
|
405
|
+
const session = mkdtempSync(resolve(stagingRoot, PREMIUM_PACK_ID + '-'));
|
|
406
|
+
const candidate = resolve(session, 'candidate'); mkdirSync(candidate, { mode: 0o700 });
|
|
407
|
+
const backup = guardPersonaPath(home, resolve(dirname(root), '.' + PREMIUM_PACK_ID + '.backup-' + randomUUID()));
|
|
408
|
+
const receiptPath = guardPersonaPath(home, premiumPackReceiptPath(home));
|
|
409
|
+
const priorReceipt = existsSync(receiptPath) ? readFileSync(receiptPath) : null;
|
|
410
|
+
let backedUp = false, promoted = false;
|
|
411
|
+
try {
|
|
412
|
+
const archive = await acquireWebsitePersonaArchive(credentials, remote, options);
|
|
413
|
+
if (archive.length !== remote.release.size) throw new Error('Persona archive size does not match the release');
|
|
414
|
+
if (createHash('sha256').update(archive).digest('hex') !== remote.release.sha256) throw new Error('Persona archive digest does not match the release');
|
|
415
|
+
extractPersonaZip(archive, candidate);
|
|
416
|
+
// Trust remains in the core verifier, independent of transport/grant success.
|
|
417
|
+
const verified = validatePersonaPack(candidate, { providerId: 'wordpress-edd', revision: remote.release.sha256 });
|
|
418
|
+
if (verified.manifestVersion !== remote.release.version || verified.personaPath !== 'premium-personas' || verified.compatibility !== 'current') throw new Error('Persona archive manifest differs from the release');
|
|
419
|
+
guardPersonaPath(home, root); guardPersonaPath(home, receiptPath);
|
|
420
|
+
if (existsSync(root)) { renameSync(root, backup); backedUp = true; }
|
|
421
|
+
renameSync(candidate, root); promoted = true;
|
|
422
|
+
if (options.failAfterStage === 'candidate-promoted') throw new Error('Injected premium persona promotion failure');
|
|
423
|
+
atomicReceipt(home, { ...receiptFor(verified, options.now), installationId: randomUUID(), provenance: {
|
|
424
|
+
server: remote.server, seatId: remote.seatId, subscriptionId: remote.subscription.id, planType: remote.subscription.planType,
|
|
425
|
+
} });
|
|
426
|
+
if (options.failAfterStage === 'receipt-written') throw new Error('Injected premium persona receipt failure');
|
|
427
|
+
if (backedUp) { rmSync(backup, { recursive: true }); backedUp = false; }
|
|
428
|
+
return { schema: 'ewai.premium-sync/v1', status: local.installed ? 'updated' : 'installed', provider: 'wordpress-edd',
|
|
429
|
+
revision: verified.revision, version: verified.manifestVersion, contentDigest: verified.contentDigest };
|
|
430
|
+
} catch (e) {
|
|
431
|
+
if (promoted) rmSync(guardPersonaPath(home, root), { recursive: true });
|
|
432
|
+
if (backedUp) renameSync(guardPersonaPath(home, backup), root);
|
|
433
|
+
if (priorReceipt) { guardPersonaPath(home, receiptPath); writeFileSync(receiptPath, priorReceipt, { mode: 0o600 }); }
|
|
434
|
+
else rmSync(guardPersonaPath(home, receiptPath), { force: true });
|
|
435
|
+
throw e;
|
|
436
|
+
} finally { rmSync(guardPersonaPath(home, session), { recursive: true }); }
|
|
437
|
+
});
|
|
438
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export function personaLicencePath(home = homedir()) {
|
|
7
|
+
return resolve(home, '.ewai/entitlements/conversational-coding.json');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function personaAccessDenialPath(home = homedir()) {
|
|
11
|
+
return resolve(home, '.ewai/entitlements/persona-access-denial.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readPersonaAccessDenial(home = homedir()) {
|
|
15
|
+
return privateJson(home, personaAccessDenialPath(home));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Shared by EWAI and future Monomyth clients: identity is not a hostname hash.
|
|
19
|
+
export function personaInstallationPath(home = homedir()) {
|
|
20
|
+
return resolve(home, '.config/conversational-coding/installation.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function guardPersonaPath(home, path) {
|
|
24
|
+
const root = resolve(home), target = resolve(path), rel = relative(root, target);
|
|
25
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel) || !lstatSync(root).isDirectory()) throw new Error('Unsafe persona filesystem path');
|
|
26
|
+
let current = root;
|
|
27
|
+
for (const part of rel.split(/[\\/]/)) {
|
|
28
|
+
current = resolve(current, part);
|
|
29
|
+
if (!existsSync(current)) {
|
|
30
|
+
// existsSync follows links; lstat also detects a dangling link.
|
|
31
|
+
try { lstatSync(current); throw new Error('Unsafe symbolic persona path'); }
|
|
32
|
+
catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const stat = lstatSync(current);
|
|
36
|
+
if (stat.isSymbolicLink() || (!stat.isDirectory() && !stat.isFile()) || (stat.isFile() && stat.nlink !== 1)) {
|
|
37
|
+
throw new Error('Unsafe symbolic or shared persona path');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return target;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function privateJson(home, path) {
|
|
44
|
+
try { lstatSync(resolve(home)); }
|
|
45
|
+
catch (e) { if (e.code === 'ENOENT') return null; throw e; }
|
|
46
|
+
guardPersonaPath(home, path);
|
|
47
|
+
if (!existsSync(path)) return null;
|
|
48
|
+
const s = lstatSync(path);
|
|
49
|
+
if (!s.isFile() || s.size > 16 * 1024 || (process.platform !== 'win32' && (s.mode & 0o077))) throw new Error('Unsafe private persona config');
|
|
50
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); }
|
|
51
|
+
catch { throw new Error('Private persona config is invalid; repair it before continuing'); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function writePrivatePersonaJson(home, path, value) {
|
|
55
|
+
guardPersonaPath(home, path);
|
|
56
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
57
|
+
chmodSync(dirname(path), 0o700);
|
|
58
|
+
const temporary = path + '.' + randomUUID() + '.tmp';
|
|
59
|
+
try {
|
|
60
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
61
|
+
renameSync(temporary, path);
|
|
62
|
+
} finally { rmSync(temporary, { force: true }); }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readPersonaLicence(home = homedir()) {
|
|
66
|
+
const data = privateJson(home, personaLicencePath(home));
|
|
67
|
+
if (!data) return null;
|
|
68
|
+
if (data.schema !== 'ewai.persona-licence/v1' || typeof data.licenceKey !== 'string' || !data.licenceKey
|
|
69
|
+
|| !/^[a-f0-9]{64}$/.test(data.installationSecret) || typeof data.activationToken !== 'string' || !data.activationToken
|
|
70
|
+
|| typeof data.activationId !== 'string' || !data.activationId || typeof data.server !== 'string') {
|
|
71
|
+
throw new Error('Private persona config is invalid; configure the licence again');
|
|
72
|
+
}
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function personaInstallationSecret(home = homedir()) {
|
|
77
|
+
const path = personaInstallationPath(home), data = privateJson(home, path);
|
|
78
|
+
if (data) {
|
|
79
|
+
if (data.schema !== 'cce.installation/v1' || !/^[a-f0-9]{64}$/.test(data.installationSecret)) throw new Error('Private machine identity is invalid');
|
|
80
|
+
return data.installationSecret;
|
|
81
|
+
}
|
|
82
|
+
const secret = randomBytes(32).toString('hex');
|
|
83
|
+
// An exclusive file prevents two products replacing the identity concurrently.
|
|
84
|
+
guardPersonaPath(home, path); mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); chmodSync(dirname(path), 0o700);
|
|
85
|
+
try { writeFileSync(path, JSON.stringify({ schema: 'cce.installation/v1', installationSecret: secret }) + '\n', { flag: 'wx', mode: 0o600 }); }
|
|
86
|
+
catch (e) { if (e.code === 'EEXIST') return personaInstallationSecret(home); throw e; }
|
|
87
|
+
return secret;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function withPersonaMutationLock(home, operation) {
|
|
91
|
+
const folder = guardPersonaPath(home, resolve(home, '.ewai/packs'));
|
|
92
|
+
mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
93
|
+
const lock = guardPersonaPath(home, resolve(folder, '.persona-pack.lock'));
|
|
94
|
+
try { mkdirSync(lock, { mode: 0o700 }); }
|
|
95
|
+
catch (e) { if (e.code === 'EEXIST') throw new Error('Another persona-pack operation is running; retry after it finishes'); throw e; }
|
|
96
|
+
try { return await operation(); }
|
|
97
|
+
finally { guardPersonaPath(home, lock); rmSync(lock, { recursive: true }); }
|
|
98
|
+
}
|