Conversation
| // Removes "Bearer " and trims | ||
| const token = authorizationHeader.split(' ')[1].trim(); | ||
| // Load user for later middleware/routes to use | ||
| req.user = await User.newUser({ token }); |
There was a problem hiding this comment.
User.newUser is misleading, it does not create a new user, instead it retrieves a user based on the JWT token.
There was a problem hiding this comment.
User.fromToken({ token }) or User.getByToken({ token }) would be a better fit.
There was a problem hiding this comment.
Yup agreed. I was using existing methods though.
| const appDefinition = backendJsFile.Definition; | ||
|
|
||
| const backend = createFriggBackend(appDefinition); | ||
| const loadRouterFromObject = (IntegrationClass, routerObject) => { |
There was a problem hiding this comment.
"Object" is too vague, I have no clue what can be in there or what that means
There was a problem hiding this comment.
I think we could call it loadIntegrationRoutes
There was a problem hiding this comment.
Also the routerObject is already inside of IntegrationClass isn't it?
There was a problem hiding this comment.
The overall method of loadIntegrationRoutes makes sense, or integration defined routes.
The load from object method was intended to load from a... route object? Dunno what to call it. The "method", "path", "event" object. I'm not even sure where I got that concept from except it's a common way to represent an http endpoint. Minus the event thing. That I want to be an easy way for a dev to reference when creating event handlers.
|
|
||
| for (const routeDef of IntegrationClass.Definition.routes) { | ||
| if (typeof routeDef === 'function') { | ||
| router.use(basePath, routeDef(IntegrationClass)); |
There was a problem hiding this comment.
I don't understand this.
You're looping through IntegrationClass.Definition.routes and from every routeDef you're passing the same integration as parameter?
There was a problem hiding this comment.
Do you have an example of a place where we declare routes as a function and need it's own integration class as parameter?
There was a problem hiding this comment.
Why not stick to one type of route definition? Either function or object
There was a problem hiding this comment.
I think the rationale was that in some cases, you need to have some logic that runs outside of express route generation. https://github.com/lefthookhq/frigg-2.0-prototyping/blob/460ad99b85b5a53bac5a859de8b8d3780b85937d/backend/src/testRouter.js#L8
In other cases, relying on the normal instantiation is fine and for those you can reach for either the straight express route or the static object.
I wanted to provide a "quick and easy", "moderate complexity", "high complexity" set of options.
That said I'm not sure what I did was the way to do it.
| router[method.toLowerCase()](path, async (req, res, next) => { | ||
| try { | ||
| const integration = new IntegrationClass({}); | ||
| await integration.loadModules(); |
There was a problem hiding this comment.
We already loadModules in the IntegrationBase constructor, we could registerEventHandlers in the constructor as well.
There was a problem hiding this comment.
I think I had an issue of not wanting to load event handlers yet because of some dynamic issues? Or something else. But, happy to not duplicate invoking!
| await integration.loadModules(); | ||
| await integration.registerEventHandlers(); | ||
| const result = await integration.send(event, {req, res, next}); | ||
| res.json(result); |
There was a problem hiding this comment.
Do we really have to return the result here?
Developers will by instict call the http response when it's available to them. I was not aware that this existed until now and probably other developers won't either.
There was a problem hiding this comment.
Well, the intent was to remove the need to think about express inside the handler function. Just, do what you need, then if you need to return something, return it.
There was a problem hiding this comment.
But if someones sees a "res" object, one would not simply return and ignore "res".
BTW, one of the reasons I don't like ruby on rails is because it does too much magic and I don't know why and how 🫠
| for (const [entityId, key] of Object.entries( | ||
| integrationRecord.entityReference | ||
| )) { | ||
| const moduleInstance = |
There was a problem hiding this comment.
Are the modules not already instantiated in IntegrationBase -> loadModules() when we do:
const instance = new integrationClass({
userId,
integrationId: params.integrationId,
});
There was a problem hiding this comment.
OK, looks like loadModules doesn't actualy loads modules but it simply instantiates the module api.
There was a problem hiding this comment.
There's a difference between "load module definitions" and "load module entities into the module instance"
| integrationRecord.config.type | ||
| ); | ||
|
|
||
| const instance = new integrationClass({ |
There was a problem hiding this comment.
rename to integrationInstance
| // for each entity, get the moduleinstance and load them according to their keys | ||
| // If it's the first entity, load the moduleinstance into primary as well | ||
| // If it's the second entity, load the moduleinstance into target as well | ||
| const moduleTypesAndKeys = |
There was a problem hiding this comment.
I think this comment also does not make sense anymore, right?
There was a problem hiding this comment.
The first line does make sense. The second and third lines are for backwards compatibility, where we enforced a "primary" and "target" concept via naming conventions.
There was a problem hiding this comment.
I don't think we need to maintain that though. Let things error and people correct the errors.
| moduleClass && | ||
| typeof moduleClass.definition.getName === 'function' | ||
| ) { | ||
| const moduleType = moduleClass.definition.getName(); |
There was a problem hiding this comment.
moduleType also comes from the name?
There was a problem hiding this comment.
Yes, though we can debate that
| const integrationClassIndex = this.integrationTypes.indexOf(type); | ||
| return this.integrationClasses[integrationClassIndex]; | ||
| } | ||
| getModuleTypesAndKeys(integrationClass) { |
There was a problem hiding this comment.
I dont get what this does
There was a problem hiding this comment.
I don't think this implementation is fully what I intended. But anywho, the goal is that we grab an integration definition, and from it we can determine which api modules are part of it, which allows us to get the required module entity instances in order to create a complete integration record (TODO allow for a module to be optional and not required on creation of an integration record, potentially make them required on a per event basis, ie we throw an error/force a user to assign a connection or create a new connection if they go to use a feature that is only available if they have a specific module instance added).
The direct use case is during the management inside the frontend experience. The ui should see "user wants to create a HubSpot integration. The HubSpot integration requires two modules. The user has one module connection (entity) available to use, but needs the other one. I'll run the auth flow for the other one and then ask them to confirm the use of that new connection."
The reason I say this implementation may not be what I intended is that we should allow multiple modules of the same type to be assigned different module names so you have something like "slackUser" and "slackApp" both pointing to the slack api module.
seanspeaks
left a comment
There was a problem hiding this comment.
Did my comments to your comments and a few added comments in there
| { | ||
| "$schema": "node_modules/lerna/schemas/lerna-schema.json", | ||
| "version": "1.2.2", | ||
| "version": "2.0.0-next.0", |
There was a problem hiding this comment.
I have no idea if this should be committed 😅
| const appDefinition = backendJsFile.Definition; | ||
|
|
||
| const backend = createFriggBackend(appDefinition); | ||
| const loadRouterFromObject = (IntegrationClass, routerObject) => { |
There was a problem hiding this comment.
The overall method of loadIntegrationRoutes makes sense, or integration defined routes.
The load from object method was intended to load from a... route object? Dunno what to call it. The "method", "path", "event" object. I'm not even sure where I got that concept from except it's a common way to represent an http endpoint. Minus the event thing. That I want to be an easy way for a dev to reference when creating event handlers.
| router[method.toLowerCase()](path, async (req, res, next) => { | ||
| try { | ||
| const integration = new IntegrationClass({}); | ||
| await integration.loadModules(); |
There was a problem hiding this comment.
I think I had an issue of not wanting to load event handlers yet because of some dynamic issues? Or something else. But, happy to not duplicate invoking!
|
|
||
| getIntegrationById: async function(id) { | ||
| return IntegrationModel.findById(id); | ||
| getIntegrationById: async function (id) { |
There was a problem hiding this comment.
I really need to lock this concept into my brain. Git repo takes the entire definition of "repository" in my head.
| const integration = | ||
| await integrationFactory.getInstanceFromIntegrationId({ | ||
| integrationId: integrationRecord.id, | ||
| userId: getUserId(req), |
There was a problem hiding this comment.
I think we discussed this on our call. Likely just was throwing things that stuck, for context on why this is here.
| @@ -349,9 +375,7 @@ function setEntityRoutes(router, factory, getUserId) { | |||
| throw Boom.forbidden('Credential does not belong to user'); | |||
There was a problem hiding this comment.
I think there were moments where this failed? Dunno what those moments were/are though.
| const {ModuleConstants} = require("./ModuleConstants"); | ||
| const { ModuleConstants } = require('./ModuleConstants'); | ||
|
|
||
| class Auther extends Delegate { |
There was a problem hiding this comment.
All for it. At one point @MichaelRyanWebber and I were debating both naming and intent of the class (hi Michael! Not to rope you in but, you likely can either find the note somewhere or comment on the future improvements you/we had in mind).
| this.EntityModel = definition.Entity || this.getEntityModel(); | ||
| } | ||
|
|
||
| static async getInstance(params) { |
There was a problem hiding this comment.
Async construction.
It's a debate in nodejs world. Might be a resolved debate in node 20? But basically "if you need to await/promise something in order to instantiate a class, do you make it an async constructor, or do you create a static async instantiation method?" Aka the get
That's the root of this decision at any rate.
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 23374074 | Triggered | Generic Password | 5c1d197 | .claude/skills/frigg-canary-test/assets/harness/docker-compose.yml | View secret |
| 23374074 | Triggered | Generic Password | 8eb6309 | .claude/skills/frigg-canary-test/assets/harness/docker-compose.yml | View secret |
| 23374074 | Triggered | Generic Password | 9b7e917 | .claude/skills/frigg-canary-test/assets/harness/docker-compose.yml | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
|
There was a problem hiding this comment.
SonarCloud found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
References Daniel Klotz's PR #590 (Tier 3 framework implementation) as the in-flight realization of Phase 2. Closes gaps the prior revision flagged: - **Framework load mechanism** subsection under Tier 3 documents the actual two-seam split #590 implemented — boot-time route claiming in integration-defined-routers.js, per-instance event merging via IntegrationBase._mergeExtensions() called from initialize(). Quotes the resolution-priority cascade, fail-loud conflict policy, and the two newly-added instantiation hooks (loadRouterFromObject's per-request closure and createQueueWorker's dry-instance branch). - **Deferred from the initial implementation** subsection captures the three Phase-2 items #590 explicitly defers (per-class merge cache, worker-side getExtensionWorkers consumption, declarative route middleware seam) plus the pre-existing IntegrationBase DI gap that extensions now inherit. - **Beyond { routes, events, queues, workers } — extending the contract** subsection enumerates primitives not yet in the extension contract: crons/schedules, user actions, config options, dynamic options. Calls out crons and user actions as highest-leverage v1.1 adds. - **Core / API module boundary — worked example** subsection uses #590's findIntegrationByPortalId as a concrete teaching case for the platform-neutral-primitive-in-core vs platform-vocabulary-wrapper-in- extension rule. Cross-references the comment posted on #590 proposing the rename to findIntegrationByEntityExternalId. Also: extends Phase 2 to reference #590's actual scope, adds Phase 7 for v1.1 contract extensions, adds Open Questions 9-14 (merge cache, worker consumption, route middleware, IntegrationBase DI, contract extensions, boundary-as-guideline), updates References with a new "Framework implementation (in flight)" section pointing at #590. No code changes; documentation only.
Resolves two review threads on the Tier 3 Integration Extensions PR.
1. Entity-level ambiguity (Codex P1)
`findIntegrationByPortalId` called `moduleRepository.findEntity` which
returns only the first match. If two Entity rows share the same
`externalId`, the duplicate was silently dropped and the downstream
integration-level ambiguity check never saw it — a cross-tenant
routing risk.
- Adds `findEntities(filter)` to ModuleRepository (interface + mongo,
postgres, documentdb, legacy implementations) symmetric with the
existing `findEntity(filter)`.
- Helper now throws on entity-level multi-match in addition to the
existing integration-level multi-match.
2. Helper rename + boundary clarification (Sean)
"Portal ID" is HubSpot vocabulary. The generic primitive belongs in
core; platform-named wrappers belong inside each api-module's own
extension (HubSpot `portalId`, Slack `team_id`, Asana `workspace_id`,
Microsoft Teams `tenant_id`, etc.).
- Renames `findIntegrationByPortalId` →
`findIntegrationByEntityExternalId`.
- Adds sibling `listIntegrationsByEntityExternalId` for the
legitimate one-externalId → many-integrations fan-out case
(returns an array, does not throw on ambiguity).
- Updates EXTENSIONS.md with the new helper signatures and a worked
example of the core / api-module-extension boundary rule.
3. SonarCloud version disclosure
Suppresses Express `X-Powered-By` header on the test harness in
`integration-defined-routers.test.js`.
Tests: 15 new + reworked unit tests for the renamed helper and its
list sibling — covering entity-level ambiguity, integration-level
ambiguity, orphan entities, deduplication across multiple matched
entities, and moduleName forwarding. All pass. Existing
`integration-defined-routers` suite still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Tier 3 extension flow needs the SQS worker to dispatch on the extension-declared event name (e.g. HUBSPOT_WEBHOOK) so the bound integration handler fires. Before this change, queueWebhook hardcoded event: 'ON_WEBHOOK' in the SQS message body and the dispatcher always routed to the default onWebhook handler — extension-bound handlers never executed end-to-end. Now: callers may pass `event` in the payload; it's stripped from the data payload and used as the SQS message's dispatch event. Default remains 'ON_WEBHOOK' so the existing Definition.webhooks: true path behaves identically. Caught by the sub-agent building the matching HubSpot extension bundle in api-module-library#91. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Proposes an integration-level Capability Declaration on IntegrationBase.Definition.capabilities, extending the API-module-level pattern from ADR-EXTENSIONS to the integration class. Capabilities are descriptive (not generative), use the same backedBy + implementedBy vocabulary as ADR-EXTENSIONS, and resolve via prototype chain with inherits/override semantics. Key additions specific to the integration layer: - Primitive taxonomy (dataSync, workflow, userAction, configOption, lifecycleHook, webhookHandler, fenestraComponent, cron, aiInference, apiProxy, mcpTool) - `surface` field on implementedBy (default / vendor-webhook / vendor-frontend / admin / internal) with auth model + route shape per surface - Surface-selection rule: prefer default-surface primitives (getConfigOptions, USER_ACTION events, /api/v2/integrations) over declaring new routes; vendor-namespaced surfaces reserved for cases where the external system literally cannot reach default - Pointer discipline — declarations point at code by name, never claim behavior Includes a worked PipedriveIntegration example showing 8 current routes collapsing to 2 own capabilities + 11 inherited (5 from BaseCRMIntegration, 6 from IntegrationBase). Status: PROPOSED. Draft committed to preserve work-in-progress on the overhaul branch while adversarial review is in flight. Known issues being addressed in the next revision: - Static validation of `implementedBy.events` requires either a static EventDefinitions field or admission that the lint is dynamic (this.events is constructor-wired today) - `implementedBy.extensions` cross-reference needs to pin to local binding keys, not extension class names - Multi-`via` capabilities need per-field discrimination (artifact surface vs Frigg-side backend) - `surface: vendor-frontend` discriminator should likely reframe around credential carrier (Bearer / vendor-JWT / vendor-signature / none) rather than "where the UI runs" - Companion ADRs (ontology-layers, agent-harness, evals) not yet drafted Not yet ready for merge or PR to next. https://claude.ai/code/session_01Wbyh477BTZHfMLQMzASnuc
…mmands
Daniel's review on api-module-library#91: integration instances should not
expose findIntegrationByEntityExternalId directly. Cross-cutting lookups
belong in friggCommands — the canonical access pattern, consistent with
how findIntegrationContextByExternalEntityId, loadIntegrationContextById,
etc. are already shaped.
This commit:
- Extracts the reverse-lookup logic from IntegrationBase instance methods
into two use cases:
FindIntegrationByEntityExternalIdUseCase
ListIntegrationsByEntityExternalIdUseCase
- Exposes both via createIntegrationCommands so they're available as
integration.commands.findIntegrationByEntityExternalId(...)
- Removes the instance methods from IntegrationBase (single source of
truth, no duplicate access surface)
- Moves the test file alongside the use cases and rewrites the tests to
target the use case classes directly (mock the two repos, no
IntegrationBase scaffolding required)
- Updates EXTENSIONS.md to show the commands-based access pattern and
the api-module-side wrapper signature
Behavior is unchanged — same throws on entity-level / integration-level
ambiguity, same return shapes. Only the access path moved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rial review Four parallel reviewers (architectural coherence, Frigg API alignment, agent-eval value, migration & drift) surfaced enough factual and design errors that the draft needed a substantial rewrite. Major corrections: - Replace single `consumedBy` with `surface` + `auth` split. Surface describes where the endpoint is mounted (default, vendor-webhook, vendor-namespaced, admin, internal, mcp); auth describes which credential carriers it accepts (bearer, adopter-jwt, shared-secret, admin-key, vendor-signature, vendor-jwt, none). The earlier single-dimension framing collapsed surface and auth and led to the hand-wavy "vendor-frontend" enum value (now dropped). - Fix all `/api/v2/...` references to actual current v1 paths. ADR-006 marks Phase 1-4 as accepted but no v2 router exists in the current checkout. Worked examples now cite the real v1 endpoints (`PATCH /api/integrations/:integrationId`, `/api/integrations/:id /actions/:actionId`, etc.). - Replace references to `loadUser` / `requireLoggedInUser` / `requireAdmin` middleware. Those files live on `feature/integration-router-v2-drop-modules-router`, not on this branch. Actual current code uses inline `authenticateUser.execute()` inside handlers (packages/core/user/use-cases/authenticate-user.js :49-99), supporting four user-context auth methods, not one. - Rebuild the Pipedrive worked example from the actual current four-route state (not the eight-route fiction from stale search results). Only `/uninstall` survives as a route after the cleanup per the May 25 #dev-feed thread; `callActivityDestination` and the admin script ride default-surface primitives with no new routes. Three own capabilities total; one route remaining. - Split Phase 2 into Phase 2 (framework-only, IntegrationBase declaration) and Phase 2.5 (adopter base classes such as BaseCRMIntegration and BaseOutboundIntegration, which live in adopter repos and are not framework deliverables). - Address the static-check viability gap. `this.events` is wired in constructors, so `validateCapabilityImplementation` cannot be purely static today. Propose a `static EventDefinitions` convention as a paired framework change; mark as Open Question 1. - Pin `implementedBy.extensions` cross-references to binding keys (the locally-unique identifier in Definition.extensions), resolving the ambiguity flagged in architectural review. - Drop multi-`via` as the default pattern. A capability with both Artifact and Frigg-side handler splits into sibling capabilities. Multi-via is now Open Question 4. - Constrain `description` to purpose-not-behavior. Every worked- example description rewritten. Behavior claims are forbidden in the Definition — that's the pointer discipline. - Add `extend` (array append) and `disinherit` to inheritance semantics. The earlier shape only supported `override` (shallow replace), which the architectural reviewer flagged as forcing authors to redeclare unchanged sibling fields when they only meant to add. - Gate Phase 2 on PR #590 merged + one prerelease cycle stable. Gate Phase 5 on eval falsification criteria from ADR-EVALS (capability-only condition <8pp lift at Sonnet OR Haiku <70% at all-three-enabled triggers reconsideration). - Add Revision history section documenting what changed and why, with provenance back to the four review findings. Open Questions 5, 6, 9, 10 added; 1-4 and 7-8 updated to reflect the corrected design. The shape of the design (capability array, primitive + backedBy + implementedBy, pointer discipline, inheritance via prototype chain) survives. The factual layer that referenced auth middleware, v2 paths, and an eight-route Pipedrive was wrong; this revision corrects it. Not yet PR-ready. Three companion ADRs (ontology, harness, evals) remain to draft.
Companion to ADR-INTEGRATION-CAPABILITIES. Defines the four-layer ontology model (L1 universal non-obvious / L2 Frigg-context reading discipline / L3 vendor and domain / L4 live-read instance) and the in-house Node compiler that renders task-scoped subsets into XML-tagged context blocks. Key design decisions captured: - Layer model stratifies by scope-of-applicability, not subject matter. More-specific layers override less-specific; locked constraints accumulate and survive overrides. - Authoring discipline: imperative-verb constraints; pointer-only references to live code via retrieve_from; no behavioral facts about code in the ontology (behavior is the property of code, surfaced live). - Node-based compiler shipped as @friggframework/ontology from the same monorepo. ~300 LOC core. CLI plus SDK. git+https URLs for remote ontology roots with version pinning at task start. - Session protocol: pin ontology version at task start, never refresh mid-task. Refresh happens at task boundaries. Patch/minor bumps auto-adopt; major bumps (locked-rule changes) surface for review via ontology diff. - Composition with ADR-INTEGRATION-CAPABILITIES is explicit and non-overlapping: capabilities answer what an integration DOES; ontology layers answer what an agent NEEDS TO KNOW to work on it. L4 entries are mostly pointers to capability declarations; the ontology teaches the agent how to find them, not what they say. - L2 captures the surface-selection rule that surfaced in the May 24-25 dev-feed thread on Pipedrive routes. That rule was previously implicit institutional knowledge with no home. - Adopter-portable: framework ships L1 + L2; adopter repos extend with L3 (vendor backstory) and L4 (instance pointers). Phases land sequentially: schema + validator -> compiler -> drift tooling -> seed framework L1+L2 -> adopter L3 -> harness wiring (ADR-AGENT-HARNESS) -> eval measurement (ADR-EVALS). Eight open questions captured; falsification criteria deferred to ADR-EVALS. Not yet PR-ready. ADR-AGENT-HARNESS and ADR-EVALS remain to draft; fourth adversarial reviewer (agent-eval value) still in flight.
Wires ADR-INTEGRATION-CAPABILITIES and ADR-ONTOLOGY-LAYERS into a Claude Code SessionStart hook plus an MCP server fallback. The harness has three responsibilities: 1. Session-start injection. Reads the repo's harness config, runs resolveCapabilities(IntegrationClass) and compileContext(), and emits a single <FRIGG-HARNESS-CONTEXT> block with <ONTOLOGY> and <CAPABILITIES> sub-blocks for Claude Code to prepend as a system- prompt overlay. 2. Task pinning. First run for a new task writes .frigg-harness/ state.json with the resolved ontology version + capability class hash + compiled block hash. Subsequent runs (including subagent invocations) reuse the pinned state. Mid-task drift is impossible by construction; default hash-mismatch policy is warn-and-recompile with stricter / laxer modes available. 3. Subagent propagation. spawnSubagentPrompt() helper templates the parent's pinned context into child prompts. A phase-executor subagent inherits the orchestrator's pinned ontology and capability versions; it never re-pins. Optional fourth responsibility: MCP server fallback exposing the same content via mcp__frigg-harness__get_capabilities(), get_ontology(), pin_task(), resolve_pointer() tool calls. Same content, pull-based instead of push-based, for agents outside Claude Code's SessionStart-hook contract. Active-integration detection is heuristic (auto-from-branch, auto- from-cwd, explicit override). Failure modes default to graceful degradation: missing config, unreachable ontology root, broken capability declaration all warn-and-proceed rather than block. The exception is locked-content budget overflow, which is a hard error because locked constraints cannot be elided. Two transports (SessionStart push vs MCP pull) ship together. The eval (ADR-EVALS) measures which produces better task accuracy at each model capability tier. The harness on/off is one of three independent variables in the eight-condition matrix. Phases land sequentially: package skeleton -> SessionStart hook -> task pinning -> subagent propagation -> MCP fallback -> failure-mode tests -> eval integration. Gates: Phase 2 of this ADR depends on the resolver from ADR-INTEGRATION-CAPABILITIES and the compiler from ADR-ONTOLOGY-LAYERS both shipping first. Eight open questions captured; relationship to the eval ADR's falsification criteria is forward-referenced. Not yet PR-ready. ADR-EVALS remains to draft; fourth adversarial reviewer (agent-eval value) still in flight.
Completes the four-ADR set. Defines the eight-condition factorial matrix that tests the three independent variables (capability declaration, ontology layers, agent harness) against a five-model ladder spanning Haiku-class through Opus-class plus cross-vendor and open-weights. Falsifiable hypothesis with six explicit criteria gating Phase 5 of ADR-INTEGRATION-CAPABILITIES (rolling migration of remaining integrations across adopter repos): 1. All-three at Haiku >=80% composite (weak-model promise) 2. Capability-only at Sonnet >=15pp lift over baseline 3. Ontology-only at Sonnet >=10pp lift over baseline 4. Harness-only at Sonnet >=10pp lift over baseline 5. Combined-effects monotonicity (no variable degrades score) 6. Locked-constraint adherence >=95% under prompt pressure Missed criteria 1-4 by >5pp = pause rolling migration; missed criteria 5-6 at all = stop and reconsider design. The shape of the design survives single missed thresholds (tuning available); deeper failures invalidate the value claim. Ten-task seed mixing three categories: - 5 generic tasks against synthetic fixture integrations - 3 real-bug-derived tasks (incl. the Pipedrive /settings smell from the May 24-25 dev-feed thread) - 2 adversarial locked-constraint pressure tests Tasks are scored on four independent dimensions (correctness, locality, constraint adherence, cost) and combined via weighted composite. Per-task weight override available for adversarial tasks where constraint adherence should dominate. Build on promptfoo (Node-native, MIT, mature) with a custom provider for the Frigg-agent-in-sandbox loop. ~1500 LOC total including tests, scorers, fixtures, custom provider. Three-layer ownership: - @friggframework/evals: scorers + fixture integrations + generic tasks; published from the Frigg monorepo - lefthookhq/quo--frigg/evals/ (and other adopters): real- integration tasks, importing scorers from the framework package - Framework eval numbers are load-bearing for generality claims; adopter numbers are illustrative real-world demonstration Cadence: - Every PR: smoke test only (parse, validate, compile - no model calls; ~free, fast) - On-demand: workflow_dispatch with --models, --tasks, --conditions knobs; ~$10-50 typical iteration - Canonical at milestones: full 8x5x10 = 400 runs; $50-150 per canonical; committed to evals/results/ and inserted into this ADR's Canonical Results section - Local dev: single-cell runs <$1 Rejected alternatives: - Python-based Inspect AI: cross-language toolchain tax - Adopter-only evals: can't make generality claims from one adopter's biased data - Skip eval, ship on intuition: how the original integration-definition.schema.json shipped with zero adopters - Eval on every PR: cost and latency untenable Ten open questions captured, including task-set evolution, model-version drift, scorer weight tunability, agent-loop architecture (single-loop vs multi-agent simulation), per-condition baseline (with vs without existing CLAUDE.md), and statistical methodology (effect size + significance, not just p-values). Canonical results section is templated; populated after first run following ADR-INTEGRATION-CAPABILITIES Phase 4. None of the four ADRs land on `next` until the canonical run exists. Not yet PR-ready. Fourth adversarial reviewer (agent-eval value) remains in flight; if/when it returns its findings inform a follow-up revision to this ADR specifically.
- Scrub client-specific (Quo) and private-repo references; generalize vendor examples to HubSpot / Pipedrive / Salesforce / Slack. - Add Module-exported ontology: api modules ship their own vendor L3 fragments; integration projects compose from installed modules. - Add Source adapters: compiler converts md / gdoc / notion / github / live sources to canonical records on the fly; distinguish source (authoring-time) from retrieve_from (runtime). - Add Validation subagent pattern: recommended pass that spawns a subagent with the same compiled block to review parent agent output. - Add Friction capture and ontology evolution: feedback loop that captures ambiguity / gaps / conflicts surfaced by agents and the validation subagent; backlog becomes the maintenance trigger. - Reframe Phase 5 around module-exported ontology; add Phase 6 (source adapters), Phase 8 (friction tooling), renumber accordingly. - Update Negative consequences to reference friction loop as the named defense against the "seed commits, ongoing edits stall" pattern. - Add open questions on friction-tool naming and module-ontology rollout.
ADR-AGENT-HARNESS:
- Add Lifecycle taxonomy subsection mapping the 9-phase agent lifecycle
(Freya ADR-008) onto Frigg touchpoints; clarify Frigg implements only
the two phases Claude Code's hook surface exposes (SessionStart for
parent pre_turn, SubagentStart for subagent pre_turn) and uses port
decorators or skill conventions for the rest.
- Add decorator-vs-hook framing (decorators wrap ports for structural
concerns; hooks punctuate the lifecycle for cross-cutting concerns).
- Add Shared friction pipeline subsection noting the @freyaframework/
friction package as a co-owned dependency.
ADR-ONTOLOGY-LAYERS:
- Revise Validation subagent pattern to use Freya's discriminated-union
return shape: {valid | fixable, correctionPrompt | friction, events}.
FRICTION classification emits events into the shared pipeline.
- Replace abstract friction-tooling section with explicit borrow from
Freya ADR-009 as @freyaframework/friction shared package. Itemize the
v1 consumed surface (typed FrictionEvent with 6 starting signals,
OntologyVcsPort + GitHubOntologyVcsAdapter, discriminated-union
validation, threshold+corroboration+decay) and the v1 deferred surface
(MigrationPlan, persisted FrictionCluster, confidence float, scheduled
aggregator, multiple separate detection hooks, eight domain events).
- Lift the five non-negotiable guardrails verbatim (harness-side-only
detection, human-approve-only, decay, rejection cooldown, cross-tenant
isolation).
- Add positioning: bottom-up schema synthesis as the structural inverse
of Hermes-style procedure synthesis.
- Update Open Q9 from "tool TBD" to "shared package decided; confirm
trimmed v1 surface with Freya ADR-009 roadmap."
…ion package - Generalize lefthookhq/quo--frigg and other client-repo references in ADR-INTEGRATION-CAPABILITIES, ADR-AGENT-HARNESS, and ADR-EVALS to "adopter repo / canary adopter project / participating adopter projects". The Frigg framework's ADRs are public; private client work doesn't belong inline. Vendor names (HubSpot, Pipedrive, Salesforce, Slack) preserved as examples since they're shipping api modules. - Rename transformPersonToQuo example to transformPersonToDestination. - Rename the second module in the worked example from "quo" to "destination" to preserve the renaming idiom without naming a client. - Add cross-link from ADR-EVALS to ADR-ONTOLOGY-LAYERS friction subsection, noting eval miss-cases feed the shared @freyaframework/friction package as FrictionEvents.
…tByExternalEntityId The use case was incorrectly finding integrations by userId, which would return wrong results when a user has multiple integrations. Now uses findIntegrationsByEntityId to correctly find the integration that contains the specific entity. Fixes FRI-498 https://claude.ai/code/session_01EQCPbqP3QHZbvqPy3fW5yE
The parameter name now matches the entity property name. https://claude.ai/code/session_01EQCPbqP3QHZbvqPy3fW5yE
When an entity belongs to multiple integrations of different types, we need to filter by config.type to return the correct one. https://claude.ai/code/session_01EQCPbqP3QHZbvqPy3fW5yE
…olve-route fix(devtools): expose POST /admin/db-migrate/resolve route (fixes #626)
Treat reporting as a sibling of the Admin Script Runner (ADR-005) on shared primitives, replacing PR #607's standalone reporting silo. Reports register via `reports: []` in the app definition (plus `admin.includeBuiltinReports` for core built-ins), extend `ReportBase`, and run through the same runner, admin API key, async/SQS execution, and EventBridge scheduling as admin scripts. Run modes: live (compute inline, persist nothing), recorded (persist an execution record), snapshot (recorded run tagged into a named series). All three use the isolated `AdminScriptExecution` store (type: 'REPORT', no user/ integration FK), so a user-scoped query can never return a report record. Highlights: - ReportBase + built-in `integrations` report (PR #607 aggregation preserved, schemaVersion 1) reading via the admin command bundle, not a repository. - Fold the retired reporting repository triad into the canonical integration / integration-mapping repositories (findAllForReport, countByIntegrationIds); hoist the DocumentDB cursor-drain into documentdb-utils (findManyDrained / aggregateDrained) so deployment-wide scans never truncate at the first batch. - report-commands (type:'REPORT', guarded findExecutionById, findSnapshotSeries), report-runner, report-router (live + async + snapshots + executions + schedule), report-executor-handler, bootstrap wiring with a script/report name-collision guard. - devtools AdminScriptBuilder: dedicated ReportQueue + report executor Lambda, report router, artifact bucket + scheduler resources gated on reports; remove the always-on standalone reporting function. - Artifact storage (S3 + signed URL, private + SSE) for non-JSON report output. - Credential-refresh generic (countActiveByType) reading only a non-secret projection — never decrypts credential secrets. Retire the silo: delete reporting-router, reporting/repositories/*, use-cases/list-integrations-report, handlers/routers/reporting; collapse the dedicated reporting API key into the admin API key. Reviewed across three adversarial passes; confirmed findings fixed (artifact dev-stage selection, DocumentDB truncation, execution-record compensation on enqueue/requeue failure). NOTE: adds @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner to packages/core/package.json — package-lock.json must be regenerated with the repo's canonical npm 10 before merge (do not commit an npm 11 lockfile). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Reports (ADR-010) section to the frigg skill covering the reports registry, ReportBase, run modes (live/recorded/snapshot), endpoints, artifact output, and scheduling, with two examples: a snapshot+scheduled JSON report reading via the frigg command bundle, and a recorded CSV report writing to artifact storage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…isolation) Follow-up to ADR-010 reporting, resolving verified code-review findings: - Artifact retrieval: report execution + snapshot reads now mint a signed download URL (report-commands) so stored non-JSON artifacts are reachable via the API; snapshot artifactUrl is a real URL, not the raw storage ref. - Async run validation: recorded/snapshot runs validate mode + inputSchema before persisting/enqueueing, returning 400 up front instead of a 202 that fails later in the worker. - Isolation: admin-script findExecutionById now rejects REPORT rows, symmetric with report findExecutionById rejecting non-REPORT rows. - Report stays protocol-agnostic: integrations-report throws a coded INVALID_INPUT error instead of Boom (no HTTP in the application layer). - Add zip artifact content type; remove orphan findMany import; correct the local artifact adapter comment. - Docs: clarify that a Definition-declared schedule only supplies the default mode; the recurring trigger is activated via PUT /:name/schedule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…unwrap helper Reduce PR-added code comments across the reporting stack so the code speaks for itself (~700 -> ~290 comment lines), keeping only non-obvious "why": invariants, ordering/consistency reasons, and gotchas. Removed route-path JSDoc banners, architecture-tag headers, and param/flow restatements. Comment-only changes; no runtime behavior altered. Also drop the unwrap() helper in integrations-report: read commands directly so the report body reads plainly. A failed read still fails the report (the runner records FAILED), just without unwrap's synthesized message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ns-plan-5b607f feat(core): implement ADR-010 reporting as an admin operation
PR #628 added @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner to packages/core for report artifact storage, but the root package-lock.json was never regenerated. The Release workflow's `npm ci` fails with EUSAGE ("Missing: @aws-sdk/s3-request-presigner ... from lock file"), so no 2.0.0-next.105 has published since #628 merged. Regenerated with npm 10 (npm 11 lockfiles break `npm ci` in this repo's CI). Verified: `npm ci --dry-run` now resolves cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ws-sdk fix: sync package-lock.json with @aws-sdk artifact-storage deps
Migrate and redesign the friggframework.org marketing site under website/, add the on-site "Ask Freya" assistant running on a vendored Freya runtime with a Frigg-domain ontology and roadmap-retrieval tools, apply Frigg brand green, and exclude the vendored bundle from SonarCloud.
✅ Deploy Preview for friggframework-org canceled.
|
When a module reports CREDENTIAL_INVALIDATED, receiveNotification flipped the integration to ERROR via persistStatus, which writes only the status column. The reason reached the log line and nothing else, so the integration surfaced to end users and support as broken with an empty errors array, diagnosable only by a log query pinned to the flip timestamp. In one adopter's production database this accounted for 74 of 333 ERROR integrations (22%), accruing 1-4 per day across five different integration types, each of them a genuine expired or revoked authorization. receiveNotification now calls recordCredentialRejection first, reusing the 'Authentication Error' title and voice testAuth already writes so both the passive check and the delegate produce a consistent diagnostic. Two deliberate constraints. The delegate's `reason` is not persisted: it is the FetchError message, which embeds the serialized request including the Authorization header, since FetchError blanks that only when STAGE is not dev — and these messages are shown to end users. Only the status code crosses over. And the write is best-effort, so a failed diagnostic can never prevent the status flip that stops processing on dead credentials, with recording ordered first so a failed flip still leaves a cause. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…per private
Follow-ups from the two-axis review of this branch.
Standards axis:
- Rename recordCredentialRejection to _recordCredentialRejection. Every
internal helper in this file is underscore-prefixed; an unprefixed name on a
widely-subclassed base class claims subclass API surface it does not want.
- Extract _authErrorMessage, now the single source of the "credentials no
longer work" wording. testAuth and the delegate had two near-identical
copies of the same user-facing sentence in one file; they can no longer
drift. This also normalizes the embedded newline testAuth's copy carried.
- Resolve the module name once in the delegate branch and pass it down, so the
log line and the persisted message can never name the module differently.
Drops the getName()/name walk over the notifier, and falls back to the
payload's moduleName.
- Read naturally when no module name is resolvable ("your Entity") instead of
rendering "your unknown Entity" to a user.
Spec axis found that two token-grant paths dropped the failure entirely:
getTokenFromUsernamePassword and getTokenFromClientCredentials called
notify(DLGT_INVALID_AUTH) with no payload from a bare `catch {}`, so the
delegate had no status code to persist. Both now forward the caught error.
Note the review characterized those as the token-refresh path; they are not.
refreshAccessToken has no notify site at all — its error propagates to
refreshAuth, which only logs and returns false. The refresh case reaches the
delegate via the next request's 401 in Requester._invalidateAuth, which
already passes a FetchError, which is why observed production flips do carry
a status code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Module.validateDefinition throws when moduleName is missing, and it runs in the constructor before this.name is assigned, so no Module instance can carry a falsy name. Delegate.notify always passes that instance as the notifier, and testAuth's caller passes a constructed module too. Both fallbacks — the 'your Entity' branch and the payload's moduleName — were therefore dead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Passing the caught error to notify(DLGT_INVALID_AUTH) leaked credentials.
The bodies of these two grant requests hold the raw password and the raw
client secret, FetchError embeds the request body in its message whenever
STAGE is dev, and both markCredentialsInvalid and receiveNotification log
that message. Before this branch these sites notified with no payload, so
the logs stayed empty; forwarding the error turned dev deployments into a
place where end-user passwords and client secrets reach CloudWatch.
Forward { statusCode } instead. The delegate only ever needed the status to
build its message, and the tests now assert the secret cannot reach the
payload rather than just asserting the shape.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refreshAuth's docstring has always claimed "On failure, notifies delegates
via DLGT_INVALID_AUTH", but its catch only logged and returned false. Three
tests asserted the documented behaviour and had been red on next for as long
as they have existed.
It now notifies, forwarding { statusCode } rather than the caught error:
refreshAccessToken's body carries client_secret in a URLSearchParams that
FetchError explicitly stringifies into its message outside prod, so passing
the error would log the secret. Added a test that pins that.
The three stale assertions expected a bare notify(DLGT_INVALID_AUTH) with no
payload. That expectation was already wrong for the 401 retry test, which
reaches the delegate through Requester._invalidateAuth and has been passing a
FetchError since that method was introduced. All three now assert the payload.
Note this makes the delegate fire twice on the only path that calls
refreshAuth: Requester also calls _invalidateAuth when refreshAuth returns
false. The end state is identical since markCredentialsInvalid and the ERROR
flip are both idempotent, but it doubles the writes. Guarding
markCredentialsInvalid on credential.authIsValid === false would collapse
that, and would also damp the observed production case of one integration
firing this delegate six times in eight seconds — left for a separate change.
Core package: 1590 passing, up from 1586, with the 12 remaining suite
failures byte-identical to next.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Making refreshAuth notify means the delegate now fires twice per refresh failure, because Requester also calls _invalidateAuth when refreshAuth returns false. Each firing wrote a message and a status, so a single failure cost four writes for a state it had already reached. Guard the CREDENTIAL_INVALIDATED branch on the integration already being in ERROR, mirroring the CREDENTIAL_VALIDATED branch's existing `if (this.status !== 'ERROR') return`. Deliberately guarded here and not in Module.markCredentialsInvalid on `credential.authIsValid === false`, which was the first suggestion. Two integrations can share one Credential row — observed in production, where two Zoho integrations shared credential 12369. A credential-level guard would let the first integration's 401 mark the row invalid, then return early for the second, leaving it ENABLED with a dead credential: the same silent-broken state this branch exists to fix. Integration status is per-integration, so the guard belongs here. A test pins that a second integration sharing the credential still flips. This does not deduplicate concurrent invocations, which each hydrate their own instance and read status ENABLED before any write lands. It removes the sequential duplicate, which is the guaranteed one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d-diagnostic fix(core): record why credentials were rejected before flipping to ERROR
A production Frigg app silently orphaned 95 records over 4 months. The trigger was a cross-invocation credential rotation race: separate Lambda invocations each hydrate the credential into memory, refresh without re-reading it, and persist with a blind last-writer-wins upsert. The loser presents an already-rotated refresh token, gets invalid_grant, flags the shared credential invalid, and the queue worker then silently acks every subsequent message. PR #636 shipped a per-instance single-flight refresh; it cannot reach a race between invocations. No existing ADR covers refresh concurrency (nearest: ADR-005 names token refresh as an admin utility, ADR-006 defines credential endpoints, ADR-009 flags refresh-on-401 as a test gap). The proposed decision is a layered stack: failure containment in the invalidation and worker paths, loser recovery by re-read-and-adopt, compare-and-swap on the credential write, core-owned token expiry with a narrow pre-flight gate, declared per-module rotation semantics, and optional jitter. Provable serialization (SQS FIFO per credential) is deferred behind a named metric. The maintainer's three candidate designs (central token service, DB-event subscription, per-API re-read flag with a dual-write cache) are evaluated in Alternatives Considered; the re-read half of the third is adopted, the rest rejected on mechanism. Status is Proposed. One open question is named for ratification, plus the default-on exception to ADR-027's opt-in bar. Numbered 031 because 028-030 are claimed by drafts on unmerged branches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same decision, same facts, same code citations, same numbers. Only the language changes: short sentences, active voice, one idea per sentence, and a terms list at the top. A checklist pass verified that every code reference, measurement, and count from the previous revision is present in the new text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ter updates UpdateProcessMetrics wrote twice per call: phase 1 applied counter increments atomically via applyProcessUpdate (jsonb_set), then phase 2 persisted derived fields (duration, recordsPerSecond, estimatedCompletion) by writing the WHOLE context/results blobs back through the legacy update(). Under concurrent callers a stale phase-2 write replayed its phase-1 snapshot over the row and permanently discarded increments other callers had landed in between. Observed in production (HubSpot<->Clockwork initial sync, 88 concurrent batch handlers): processedRecords ended exactly one 25-record chunk short of totalFetched, so the completion gate (processed >= totalFetched) never fired and the sync sat in PROCESSING_BATCHES forever. A simulation against real Postgres lost 1025 of 2200 increments under an 88-way burst with the old code, and 0 with this fix. Phase 2 now writes only the three derived paths it owns through the same atomic primitive. The derived values stay best-effort under concurrency, same as before; they just can no longer destroy counters. All three repository backends (postgres, mongo, documentdb) already support the set op. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s-lost-update fix(core): write derived process metrics atomically to stop lost counter updates
Responds to the review on PR #637 (changes requested): - Cut from 1027 to 337 lines (~67%). - Restructured around the review's four options. The decision is options 4 and 2 together: a reactive database check before refresh and on 401 as the mechanism, and proactive scheduled refresh via the admin scripts as the companion. - Folded in both inline review findings: the adoption test keys on the refresh token (not the access token), and only definitive authorization rejections may invalidate a credential — transport failures (timeout, 429, 5xx) stay retryable. - Addressed the database-propagation caveat with the measured 716ms window, bounded re-read backoff, and the readPreference=primary requirement. - Compare-and-swap moved from load-bearing to deferred hardening. The serialization family stays deferred behind the named metric. - The full prior analysis remains available at commit 42d1e6f. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scheduled-refresh companion no longer requires core changes. The script hydrates each module and calls refreshAuth() serially; the existing setTokens -> DLGT_TOKEN_UPDATE -> onTokenUpdate -> upsertCredential chain persists the result with no new code. Expiry tracking (the persist/hydrate work and the null-expires_in computation fix) moves from prerequisite to optional later optimization, taken only if the bounded waste of blind refreshes ever matters. Option 4 is unchanged: the database lookup before any refresh, and the re-read-and-adopt on 401/invalid_grant, remain the mechanism and ship first. The sequence shrinks from three PRs to two. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ential-refresh ADR-031: Concurrent OAuth Credential Refresh Across Lambda Invocations
npm audit on `next` reported 68 advisories (3 critical / 34 high / 27 moderate / 4 low). Splitting dev from production showed the published surface accounted for only 10 of them, and every one had a patch available inside its existing major version. Several entries in the root `overrides` block had gone stale: the pinned version was correct when it was added, but later advisories widened the vulnerable range past it. Most notably velocityjs was pinned at 2.1.6 while the critical RCE advisory covers <=2.1.6, so the override was holding the tree on a vulnerable version. Stale pins bumped: fast-uri 3.1.2 -> 3.1.5 (<=3.1.4) ip-address 10.2.0 -> 10.5.0 (<=10.3.0) velocityjs 2.1.6 -> 2.1.7 (<=2.1.6, critical RCE) js-yaml 4.2.0 -> 4.3.1 (4.0.0-4.3.0, scoped overrides) New pins: nanoid 3.3.18 (<=3.3.17) postcss 8.5.26 (<=8.5.22) body-parser 1.20.6 (<1.20.6) js-yaml@4 4.3.1 js-yaml@3 3.15.1 (<=3.15.0) brace-expansion@1 1.1.18 (<=1.1.17) brace-expansion@2 2.1.4 (2.0.0-2.1.3) undici@6 6.28.0 (<=6.27.0) undici@7 7.29.0 (7.0.0-7.28.0) js-yaml and brace-expansion are pinned per major line rather than globally: both have multiple majors in the tree, and collapsing them would force js-yaml 3.x consumers onto v4 (which dropped safeLoad) and downgrade brace-expansion 5.x consumers. ajv, socks and @istanbuljs/load-nyc-config were flagged only for depending on vulnerable fast-uri / ip-address / js-yaml, so they clear without a direct change. Result: `npm audit --omit=dev` goes from 10 to 0. Full tree drops 68 -> 59; everything remaining is build/release tooling behind a major upgrade (lerna 8->10, nx 20->23, serverless-offline, prisma, vite) and is tracked separately. website/ has its own lockfile and already audits clean. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdchVmNyYEzYD9rBfRugko
…ss-ywp0of fix(deps): clear all production dependency vulnerabilities
|




Version
Published prerelease version:
v2.0.0-next.108Changelog
🐛 Bug Fix
@friggframework/admin-scripts,@friggframework/core,@friggframework/devtools,@friggframework/eslint-config,@friggframework/prettier-config,@friggframework/schemas,@friggframework/serverless-plugin,@friggframework/test,@friggframework/uiAuthors: 2