@agent-native/core 0.146.2 → 0.146.4
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +13 -10
- package/corpus/templates/analytics/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/analytics/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/analytics/actions/compose-dashboard.ts +53 -6
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +8 -2
- package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +3 -1
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +48 -9
- package/corpus/templates/analytics/actions/update-dashboard.ts +80 -6
- package/corpus/templates/analytics/changelog/2026-08-07-dashboard-creation-and-edits-now-save-faster-with-reliable-s.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +19 -19
- package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +21 -2
- package/corpus/templates/analytics/server/lib/bigquery.ts +59 -24
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +44 -8
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +3 -5
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +33 -69
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +90 -24
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +1 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +17 -2
- package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +2 -0
- package/corpus/templates/assets/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/assets/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/assets/netlify.toml +1 -1
- package/corpus/templates/assets/package.json +1 -0
- package/corpus/templates/assets/scripts/migrate-production.ts +24 -0
- package/corpus/templates/assets/server/plugins/db.ts +1 -1
- package/corpus/templates/assets/tsconfig.json +1 -0
- package/corpus/templates/brain/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/brain/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/brain/netlify.toml +1 -1
- package/corpus/templates/brain/package.json +2 -0
- package/corpus/templates/brain/scripts/migrate-production.ts +24 -0
- package/corpus/templates/brain/server/plugins/db.ts +1 -1
- package/corpus/templates/brain/tsconfig.json +1 -0
- package/corpus/templates/calendar/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/calendar/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/calendar/netlify.toml +1 -1
- package/corpus/templates/calendar/package.json +1 -0
- package/corpus/templates/calendar/scripts/migrate-production.ts +24 -0
- package/corpus/templates/calendar/server/plugins/db.ts +1 -1
- package/corpus/templates/calendar/tsconfig.json +1 -0
- package/corpus/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/chat/netlify.toml +1 -1
- package/corpus/templates/chat/package.json +1 -0
- package/corpus/templates/chat/scripts/migrate-production.ts +31 -0
- package/corpus/templates/chat/tsconfig.json +1 -0
- package/corpus/templates/clips/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/clips/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/clips/netlify.toml +1 -1
- package/corpus/templates/clips/package.json +1 -0
- package/corpus/templates/clips/scripts/migrate-production.ts +24 -0
- package/corpus/templates/clips/server/plugins/db.ts +1 -1
- package/corpus/templates/clips/tsconfig.json +1 -0
- package/corpus/templates/content/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/content/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/content/netlify.toml +1 -1
- package/corpus/templates/content/package.json +2 -0
- package/corpus/templates/content/scripts/migrate-production.ts +28 -0
- package/corpus/templates/content/server/plugins/db.ts +2 -2
- package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/crm/netlify.toml +1 -1
- package/corpus/templates/crm/package.json +2 -0
- package/corpus/templates/crm/scripts/migrate-production.ts +24 -0
- package/corpus/templates/crm/server/plugins/db.ts +1 -1
- package/corpus/templates/crm/tsconfig.json +1 -0
- package/corpus/templates/design/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/design/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/design/netlify.toml +1 -1
- package/corpus/templates/design/package.json +1 -0
- package/corpus/templates/design/scripts/migrate-production.ts +24 -0
- package/corpus/templates/design/server/plugins/db.ts +1 -1
- package/corpus/templates/design/tsconfig.json +1 -0
- package/corpus/templates/dispatch/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/dispatch/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/dispatch/netlify.toml +1 -1
- package/corpus/templates/dispatch/package.json +1 -0
- package/corpus/templates/dispatch/scripts/migrate-production.ts +23 -0
- package/corpus/templates/dispatch/tsconfig.json +1 -0
- package/corpus/templates/factory/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/factory/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/factory/netlify.toml +1 -1
- package/corpus/templates/factory/package.json +1 -0
- package/corpus/templates/factory/scripts/migrate-production.ts +31 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +2 -0
- package/corpus/templates/factory/tsconfig.json +1 -0
- package/corpus/templates/forms/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/forms/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/forms/netlify.toml +1 -1
- package/corpus/templates/forms/package.json +2 -0
- package/corpus/templates/forms/scripts/migrate-production.ts +24 -0
- package/corpus/templates/forms/server/plugins/db.ts +1 -1
- package/corpus/templates/forms/tsconfig.json +1 -0
- package/corpus/templates/macros/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/macros/netlify.toml +1 -1
- package/corpus/templates/macros/package.json +1 -0
- package/corpus/templates/macros/scripts/migrate-production.ts +24 -0
- package/corpus/templates/macros/server/plugins/db.ts +1 -1
- package/corpus/templates/macros/tsconfig.json +1 -0
- package/corpus/templates/mail/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/mail/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/mail/netlify.toml +1 -1
- package/corpus/templates/mail/package.json +1 -0
- package/corpus/templates/mail/scripts/migrate-production.ts +24 -0
- package/corpus/templates/mail/server/plugins/db.ts +1 -1
- package/corpus/templates/mail/tsconfig.json +1 -0
- package/corpus/templates/plan/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/plan/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/plan/netlify.toml +1 -1
- package/corpus/templates/plan/package.json +1 -0
- package/corpus/templates/plan/scripts/migrate-production.ts +24 -0
- package/corpus/templates/plan/server/plugins/00-mcp.ts +3 -2
- package/corpus/templates/plan/server/plugins/db.ts +1 -1
- package/corpus/templates/plan/tsconfig.json +1 -0
- package/corpus/templates/slides/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/slides/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +56 -23
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -0
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +4 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +91 -25
- package/corpus/templates/slides/app/hooks/use-deck-role.ts +14 -4
- package/corpus/templates/slides/app/i18n/en-US.ts +2 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +19 -7
- package/corpus/templates/slides/app/pages/Index.tsx +11 -2
- package/corpus/templates/slides/app/root.tsx +5 -1
- package/corpus/templates/slides/netlify.toml +1 -1
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/scripts/migrate-production.ts +24 -0
- package/corpus/templates/slides/server/plugins/db.ts +1 -1
- package/corpus/templates/slides/tsconfig.json +1 -0
- package/corpus/templates/tasks/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/corpus/templates/tasks/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/corpus/templates/tasks/netlify.toml +1 -1
- package/corpus/templates/tasks/package.json +1 -0
- package/corpus/templates/tasks/scripts/migrate-production.ts +24 -0
- package/corpus/templates/tasks/server/plugins/db.ts +1 -1
- package/corpus/templates/tasks/tsconfig.json +1 -0
- package/dist/action.d.ts +2 -0
- package/dist/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.js +2 -0
- package/dist/agent/thread-data-builder.d.ts +4 -0
- package/dist/agent/thread-data-builder.js +44 -0
- package/dist/automations/service.d.ts +1 -0
- package/dist/automations/service.js +21 -2
- package/dist/cli/create.js +20 -2
- package/dist/client/agent-page/AgentJobsTab.js +3 -1
- package/dist/client/agent-page/AutomationDetailsDialog.js +9 -2
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/migrations.js +7 -13
- package/dist/jobs/actions/list-automation-runs.d.ts +1 -0
- package/dist/jobs/actions/list-automation-runs.js +9 -2
- package/dist/jobs/actions/list-recurring-jobs.js +3 -1
- package/dist/jobs/actions/manage-recurring-job.js +1 -1
- package/dist/jobs/actions/run-automation-now.js +1 -0
- package/dist/jobs/background-automation-runner.js +2 -0
- package/dist/jobs/frontmatter.d.ts +2 -0
- package/dist/jobs/frontmatter.js +4 -0
- package/dist/jobs/run-history.d.ts +4 -0
- package/dist/jobs/run-history.js +28 -8
- package/dist/jobs/run-now.d.ts +4 -1
- package/dist/jobs/run-now.js +4 -3
- package/dist/jobs/scheduler.js +18 -0
- package/dist/jobs/tools.d.ts +3 -2
- package/dist/jobs/tools.js +26 -12
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.js +1 -0
- package/dist/observability/routes.d.ts +3 -3
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/action-routes.d.ts +2 -0
- package/dist/server/action-routes.js +1 -0
- package/dist/server/agent-chat-plugin.js +3 -2
- package/dist/server/builder-browser.js +24 -8
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/dist/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/dist/templates/chat/netlify.toml +1 -1
- package/dist/templates/chat/package.json +1 -0
- package/dist/templates/chat/scripts/migrate-production.ts +31 -0
- package/dist/templates/chat/tsconfig.json +1 -0
- package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/dist/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/dist/templates/default/package.json +1 -0
- package/dist/templates/default/scripts/migrate-production.ts +31 -0
- package/dist/templates/default/tsconfig.json +1 -0
- package/dist/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/dist/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/dist/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/dist/templates/workspace-root/AGENTS.md +2 -1
- package/dist/triggers/actions/list-automations.js +1 -1
- package/dist/triggers/actions/manage-automation.js +5 -1
- package/dist/triggers/actions.d.ts +1 -1
- package/dist/triggers/actions.js +21 -15
- package/dist/triggers/dispatcher.js +17 -2
- package/dist/triggers/routes.d.ts +1 -0
- package/dist/triggers/routes.js +1 -0
- package/docs/content/docs-components.mdx +122 -11
- package/package.json +1 -1
- package/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/src/templates/chat/netlify.toml +1 -1
- package/src/templates/chat/package.json +1 -0
- package/src/templates/chat/scripts/migrate-production.ts +31 -0
- package/src/templates/chat/tsconfig.json +1 -0
- package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/src/templates/default/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/src/templates/default/package.json +1 -0
- package/src/templates/default/scripts/migrate-production.ts +31 -0
- package/src/templates/default/tsconfig.json +1 -0
- package/src/templates/headless/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +7 -3
- package/src/templates/workspace-core/.agents/skills/upgrade-agent-native/SKILL.md +5 -2
- package/src/templates/workspace-root/AGENTS.md +2 -1
|
@@ -143,6 +143,27 @@ export type DeckPersistenceResult =
|
|
|
143
143
|
| { persisted: false; reason: "request-failed"; error: unknown }
|
|
144
144
|
| { persisted: false; reason: "not-found" };
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Ask the server whether a deck row exists, keeping "absent" and "could not
|
|
148
|
+
* check" distinct — a rejected write is not by itself proof the row is missing.
|
|
149
|
+
*/
|
|
150
|
+
async function probeDeckPersisted(id: string): Promise<DeckPersistenceResult> {
|
|
151
|
+
try {
|
|
152
|
+
const result = await callAction<unknown>(
|
|
153
|
+
"get-deck",
|
|
154
|
+
{ id },
|
|
155
|
+
{
|
|
156
|
+
method: "GET",
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
return normalizeActionDeck(result)
|
|
160
|
+
? { persisted: true }
|
|
161
|
+
: { persisted: false, reason: "not-found" };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { persisted: false, reason: "request-failed", error };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
146
167
|
export function describeDeckPersistenceFailure(
|
|
147
168
|
result: DeckPersistenceResult,
|
|
148
169
|
fallback: string,
|
|
@@ -178,6 +199,7 @@ interface DeckContextType {
|
|
|
178
199
|
sourceDeckId: string,
|
|
179
200
|
newId: string,
|
|
180
201
|
title?: string,
|
|
202
|
+
onFailure?: () => void,
|
|
181
203
|
) => Deck | null;
|
|
182
204
|
deleteDeck: (id: string) => void;
|
|
183
205
|
updateDeck: (
|
|
@@ -851,18 +873,52 @@ async function fetchDeckListLightFromAPI(): Promise<{ id: string }[] | null> {
|
|
|
851
873
|
}
|
|
852
874
|
}
|
|
853
875
|
|
|
876
|
+
const DECK_FETCH_RETRY_ATTEMPTS = 3;
|
|
877
|
+
const DECK_FETCH_RETRY_DELAY_MS = 400;
|
|
878
|
+
|
|
879
|
+
// `get-deck` returns a real 404/403 only when the deck is genuinely gone or
|
|
880
|
+
// the caller genuinely lacks access (see actions/get-deck.ts). A network blip
|
|
881
|
+
// or 5xx is transient and must not be coerced into the same "not found" null
|
|
882
|
+
// the caller uses to show the owner-facing "deck unavailable" pane — that
|
|
883
|
+
// flashed a wrong message on brief server hiccups even though the deck still
|
|
884
|
+
// existed.
|
|
885
|
+
function isConfirmedDeckAbsence(err: unknown): boolean {
|
|
886
|
+
const status = (err as { status?: unknown } | null)?.status;
|
|
887
|
+
return status === 404 || status === 403;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// A timeout already made the caller wait the full action timeout window once
|
|
891
|
+
// (see DEFAULT_ACTION_TIMEOUT_MS in use-action.ts); retrying would multiply
|
|
892
|
+
// that wait instead of surfacing the failure — same reasoning as
|
|
893
|
+
// `isActionTimeout` in the shared action-query retry policy.
|
|
894
|
+
function isDeckFetchTimeout(err: unknown): boolean {
|
|
895
|
+
return (err as { timedOut?: unknown } | null)?.timedOut === true;
|
|
896
|
+
}
|
|
897
|
+
|
|
854
898
|
async function fetchDeckFromAPI(id: string): Promise<Deck | null> {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
899
|
+
for (let attempt = 1; attempt <= DECK_FETCH_RETRY_ATTEMPTS; attempt++) {
|
|
900
|
+
try {
|
|
901
|
+
const result = await callAction<unknown>(
|
|
902
|
+
"get-deck",
|
|
903
|
+
{ id },
|
|
904
|
+
{ method: "GET" },
|
|
905
|
+
);
|
|
906
|
+
return normalizeActionDeck(result);
|
|
907
|
+
} catch (err) {
|
|
908
|
+
if (
|
|
909
|
+
isConfirmedDeckAbsence(err) ||
|
|
910
|
+
isDeckFetchTimeout(err) ||
|
|
911
|
+
attempt === DECK_FETCH_RETRY_ATTEMPTS
|
|
912
|
+
) {
|
|
913
|
+
console.error(`Failed to fetch deck ${id}:`, err);
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
await new Promise((resolve) =>
|
|
917
|
+
setTimeout(resolve, attempt * DECK_FETCH_RETRY_DELAY_MS),
|
|
918
|
+
);
|
|
919
|
+
}
|
|
865
920
|
}
|
|
921
|
+
return null;
|
|
866
922
|
}
|
|
867
923
|
|
|
868
924
|
export function deckIdFromPathname(pathname: string): string | null {
|
|
@@ -1776,24 +1832,18 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
1776
1832
|
}
|
|
1777
1833
|
}
|
|
1778
1834
|
|
|
1779
|
-
|
|
1780
|
-
const result = await callAction<unknown>(
|
|
1781
|
-
"get-deck",
|
|
1782
|
-
{ id },
|
|
1783
|
-
{ method: "GET" },
|
|
1784
|
-
);
|
|
1785
|
-
return normalizeActionDeck(result)
|
|
1786
|
-
? { persisted: true }
|
|
1787
|
-
: { persisted: false, reason: "not-found" };
|
|
1788
|
-
} catch (error) {
|
|
1789
|
-
return { persisted: false, reason: "request-failed", error };
|
|
1790
|
-
}
|
|
1835
|
+
return probeDeckPersisted(id);
|
|
1791
1836
|
},
|
|
1792
1837
|
[],
|
|
1793
1838
|
);
|
|
1794
1839
|
|
|
1795
1840
|
const duplicateDeck = useCallback(
|
|
1796
|
-
(
|
|
1841
|
+
(
|
|
1842
|
+
sourceDeckId: string,
|
|
1843
|
+
newId: string,
|
|
1844
|
+
title?: string,
|
|
1845
|
+
onFailure?: () => void,
|
|
1846
|
+
): Deck | null => {
|
|
1797
1847
|
if (pendingDuplicateSourceIdsRef.current.has(sourceDeckId)) return null;
|
|
1798
1848
|
const source = decks.find((d) => d.id === sourceDeckId);
|
|
1799
1849
|
if (!source) return null;
|
|
@@ -1843,10 +1893,26 @@ export function DeckProvider({ children }: { children: ReactNode }) {
|
|
|
1843
1893
|
).then(() => undefined);
|
|
1844
1894
|
pendingCreatePromisesRef.current.set(newId, duplicatePromise);
|
|
1845
1895
|
duplicatePromise
|
|
1846
|
-
.catch((err) => {
|
|
1896
|
+
.catch(async (err) => {
|
|
1897
|
+
// A rejected request is not proof the row is missing: a timeout or
|
|
1898
|
+
// dropped response can land after the server committed the insert.
|
|
1899
|
+
// Discarding the copy then would delete work that actually exists
|
|
1900
|
+
// and tell the user it failed, so confirm against the server first.
|
|
1901
|
+
const probe = await probeDeckPersisted(newId);
|
|
1902
|
+
if (probe.persisted) {
|
|
1903
|
+
console.warn(
|
|
1904
|
+
`Duplicate request for ${newId} failed but the deck persisted:`,
|
|
1905
|
+
err,
|
|
1906
|
+
);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1847
1909
|
console.error("Duplicate failed:", err);
|
|
1848
|
-
// Roll back: drop the optimistic deck from local state.
|
|
1910
|
+
// Roll back: drop the optimistic deck from local state. The caller
|
|
1911
|
+
// (via onFailure) is responsible for navigating away if the user is
|
|
1912
|
+
// still sitting on this now-gone deck's route — otherwise they're
|
|
1913
|
+
// stranded on a "Deck unavailable" screen with no way back.
|
|
1849
1914
|
setDecks((prev) => prev.filter((d) => d.id !== newId));
|
|
1915
|
+
onFailure?.();
|
|
1850
1916
|
})
|
|
1851
1917
|
.finally(() => {
|
|
1852
1918
|
pendingCreateIdsRef.current.delete(newId);
|
|
@@ -14,10 +14,18 @@ interface SharesResponse {
|
|
|
14
14
|
* `Viewer` = no edit affordances, but the editor shell is still navigable;
|
|
15
15
|
* any other role (Owner / Editor / Admin) gets full editing.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* `assumeEditorWhileLoading` should only be true when a faster, already-known
|
|
18
|
+
* signal (e.g. `deck.createdByMe`) confirms the caller is the owner — that
|
|
19
|
+
* avoids a flash of view-only chrome for owners without ever defaulting a
|
|
20
|
+
* viewer into edit affordances while the real role is still loading. Without
|
|
21
|
+
* that signal, `canEdit` stays false until the role query resolves, so a
|
|
22
|
+
* viewer can never trigger an edit that the server will reject and silently
|
|
23
|
+
* roll back.
|
|
19
24
|
*/
|
|
20
|
-
export function useDeckRole(
|
|
25
|
+
export function useDeckRole(
|
|
26
|
+
deckId: string | undefined,
|
|
27
|
+
assumeEditorWhileLoading = false,
|
|
28
|
+
): {
|
|
21
29
|
role: SharesResponse["role"] | undefined;
|
|
22
30
|
canEdit: boolean;
|
|
23
31
|
isLoading: boolean;
|
|
@@ -29,6 +37,8 @@ export function useDeckRole(deckId: string | undefined): {
|
|
|
29
37
|
);
|
|
30
38
|
const role = query.data?.role;
|
|
31
39
|
const canEdit =
|
|
32
|
-
role === undefined
|
|
40
|
+
role === undefined
|
|
41
|
+
? assumeEditorWhileLoading
|
|
42
|
+
: role === "owner" || role !== "viewer";
|
|
33
43
|
return { role, canEdit, isLoading: query.isLoading };
|
|
34
44
|
}
|
|
@@ -212,8 +212,10 @@ const messages = {
|
|
|
212
212
|
assetLibrary: "Asset Library",
|
|
213
213
|
diagrams: "Diagrams",
|
|
214
214
|
insertMermaidDiagram: "Insert Mermaid Diagram",
|
|
215
|
+
insertMermaidFailed: "Failed to insert diagram",
|
|
215
216
|
excalidrawCanvas: "Excalidraw Canvas",
|
|
216
217
|
convertMermaidToExcalidraw: "Convert Mermaid to Excalidraw",
|
|
218
|
+
convertMermaidFailed: "Failed to convert diagram",
|
|
217
219
|
removeExcalidrawCanvas: "Remove Excalidraw Canvas",
|
|
218
220
|
transition: "Transition",
|
|
219
221
|
transition_instant: "Instant",
|
|
@@ -44,7 +44,7 @@ import { QuestionFlow } from "@/components/editor/QuestionFlow";
|
|
|
44
44
|
import SlideEditor from "@/components/editor/SlideEditor";
|
|
45
45
|
import { TweaksPanel } from "@/components/editor/TweaksPanel";
|
|
46
46
|
import { Button } from "@/components/ui/button";
|
|
47
|
-
import { useDecks } from "@/context/DeckContext";
|
|
47
|
+
import { deckIdFromPathname, useDecks } from "@/context/DeckContext";
|
|
48
48
|
import { useAgentGenerating } from "@/hooks/use-agent-generating";
|
|
49
49
|
import { useDeckDesignSystem } from "@/hooks/use-deck-design-system";
|
|
50
50
|
import { useDeckPresence } from "@/hooks/use-deck-presence";
|
|
@@ -275,8 +275,10 @@ export default function DeckEditor() {
|
|
|
275
275
|
});
|
|
276
276
|
// Mirror Google Slides: viewers see the editor shell with edit affordances
|
|
277
277
|
// disabled (rather than a separate "viewer" route). Owners/Editors/Admins
|
|
278
|
-
// get the full editor.
|
|
279
|
-
|
|
278
|
+
// get the full editor. Only assume edit access while the role is still
|
|
279
|
+
// loading when `createdByMe` already confirms ownership — otherwise a
|
|
280
|
+
// viewer would briefly see (and could click) edit affordances.
|
|
281
|
+
const { canEdit } = useDeckRole(id, deck?.createdByMe === true);
|
|
280
282
|
const isNewDeckGenerating = shouldShowNewDeckGeneratingProgress({
|
|
281
283
|
generating,
|
|
282
284
|
isNewDeckCreation: wasNewDeckCreation.current,
|
|
@@ -1001,9 +1003,10 @@ export default function DeckEditor() {
|
|
|
1001
1003
|
onShowHistory={() => setHistoryOpen(!historyOpen)}
|
|
1002
1004
|
historyButtonRef={historyButtonRef}
|
|
1003
1005
|
currentSlide={currentSlide}
|
|
1004
|
-
onUpdateSlide={(updates) =>
|
|
1005
|
-
|
|
1006
|
-
|
|
1006
|
+
onUpdateSlide={(updates, slideIdOverride) => {
|
|
1007
|
+
const targetId = slideIdOverride ?? currentSlide?.id;
|
|
1008
|
+
if (targetId) updateSlide(id, targetId, updates);
|
|
1009
|
+
}}
|
|
1007
1010
|
activeUsers={slideActiveUsers.filter((u) => u.email !== session?.email)}
|
|
1008
1011
|
agentPresent={agentPresent}
|
|
1009
1012
|
agentActive={agentActive}
|
|
@@ -1025,7 +1028,16 @@ export default function DeckEditor() {
|
|
|
1025
1028
|
onToggleTextBoxMode={toggleTextBoxMode}
|
|
1026
1029
|
onDuplicateDeck={() => {
|
|
1027
1030
|
const newId = `deck-${nanoid()}`;
|
|
1028
|
-
const optimistic = duplicateDeck(id, newId)
|
|
1031
|
+
const optimistic = duplicateDeck(id, newId, undefined, () => {
|
|
1032
|
+
// The background duplicate-deck action failed after we already
|
|
1033
|
+
// navigated to the optimistic copy. If the user is still there,
|
|
1034
|
+
// send them back instead of stranding them on a "Deck
|
|
1035
|
+
// unavailable" screen for a deck that no longer exists.
|
|
1036
|
+
if (deckIdFromPathname(window.location.pathname) === newId) {
|
|
1037
|
+
navigate("/");
|
|
1038
|
+
}
|
|
1039
|
+
toast.error(t("home.duplicateFailed"));
|
|
1040
|
+
});
|
|
1029
1041
|
if (optimistic) navigate(`/deck/${optimistic.id}`);
|
|
1030
1042
|
}}
|
|
1031
1043
|
onExportPdf={async () => {
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
describeDeckPersistenceFailure,
|
|
46
46
|
type Deck,
|
|
47
47
|
} from "@/context/DeckContext";
|
|
48
|
-
import { useDecks } from "@/context/DeckContext";
|
|
48
|
+
import { deckIdFromPathname, useDecks } from "@/context/DeckContext";
|
|
49
49
|
import { useAgentGenerating } from "@/hooks/use-agent-generating";
|
|
50
50
|
import { useDesignSystems } from "@/hooks/use-design-systems";
|
|
51
51
|
import { useWorkspaceDefaults } from "@/hooks/use-workspace-defaults";
|
|
@@ -1088,7 +1088,16 @@ export default function Index() {
|
|
|
1088
1088
|
(id: string) => {
|
|
1089
1089
|
let copy: ReturnType<typeof duplicateDeck> | undefined;
|
|
1090
1090
|
flushSync(() => {
|
|
1091
|
-
copy = duplicateDeck(id, `deck-${nanoid()}
|
|
1091
|
+
copy = duplicateDeck(id, `deck-${nanoid()}`, undefined, () => {
|
|
1092
|
+
// The background duplicate-deck action failed after we already
|
|
1093
|
+
// navigated to the optimistic copy's route. If the user is still
|
|
1094
|
+
// there, send them back to the deck list instead of stranding them
|
|
1095
|
+
// on a "Deck unavailable" screen for a deck that no longer exists.
|
|
1096
|
+
if (deckIdFromPathname(window.location.pathname) === copy?.id) {
|
|
1097
|
+
navigate("/");
|
|
1098
|
+
}
|
|
1099
|
+
toast.error(t("home.duplicateFailed"));
|
|
1100
|
+
});
|
|
1092
1101
|
});
|
|
1093
1102
|
// The context refuses a second copy of the same deck while the first
|
|
1094
1103
|
// one's action is still in flight.
|
|
@@ -97,7 +97,11 @@ function useExitSelectionOnOutsideClick() {
|
|
|
97
97
|
const target = e.target as HTMLElement;
|
|
98
98
|
if (
|
|
99
99
|
target.closest(".slide-content") ||
|
|
100
|
-
target.closest(".slide-image-clickable")
|
|
100
|
+
target.closest(".slide-image-clickable") ||
|
|
101
|
+
target.closest("[data-slide-context-toolbar]") ||
|
|
102
|
+
target.closest(
|
|
103
|
+
'[role="dialog"], [role="menu"], [role="listbox"], [data-radix-popper-content-wrapper], [data-radix-menu-content]',
|
|
104
|
+
)
|
|
101
105
|
) {
|
|
102
106
|
return;
|
|
103
107
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
ignore = "node ./scripts/netlify-ignore-build.mjs slides"
|
|
3
3
|
# Rebuild Core explicitly so a cached Netlify install cannot bundle an older
|
|
4
4
|
# agent loop or A2A handler into Slides' server functions.
|
|
5
|
-
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && pnpm --filter @agent-native/core build && NITRO_PRESET=netlify pnpm --filter slides build"
|
|
5
|
+
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && pnpm --filter @agent-native/core build && NITRO_PRESET=netlify pnpm --filter slides build && if [ \"${CONTEXT:-}\" = \"production\" ]; then pnpm --filter slides migrate:production; fi"
|
|
6
6
|
publish = "templates/slides/dist"
|
|
7
7
|
functions = "templates/slides/.netlify/functions-internal"
|
|
8
8
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { closeDbExec, withMigrationRuntime } from "@agent-native/core/db";
|
|
2
|
+
import { runFrameworkReleaseMigrations } from "@agent-native/core/server";
|
|
3
|
+
|
|
4
|
+
import { runSlidesMigrations } from "../server/plugins/db.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Release-time schema entrypoint for Slides.
|
|
8
|
+
*
|
|
9
|
+
* This script is the production owner of schema changes. It runs against the
|
|
10
|
+
* direct migration endpoint selected by core, while request functions skip
|
|
11
|
+
* all migration and ensure-table work automatically.
|
|
12
|
+
*/
|
|
13
|
+
async function main(): Promise<void> {
|
|
14
|
+
await withMigrationRuntime(async () => {
|
|
15
|
+
await runFrameworkReleaseMigrations(null);
|
|
16
|
+
await runSlidesMigrations(null);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await main();
|
|
22
|
+
} finally {
|
|
23
|
+
await closeDbExec();
|
|
24
|
+
}
|
|
@@ -33,7 +33,7 @@ const schemaTables = Object.values(schema).filter(isDrizzleTable);
|
|
|
33
33
|
// packages/core/src/db/migrations.ts for the full rationale). Version numbers
|
|
34
34
|
// alone are not a safe identity across parallel branches that each extend
|
|
35
35
|
// this list independently.
|
|
36
|
-
const runSlidesMigrations = runMigrations(
|
|
36
|
+
export const runSlidesMigrations = runMigrations(
|
|
37
37
|
[
|
|
38
38
|
{
|
|
39
39
|
version: 1,
|
|
@@ -36,8 +36,10 @@ Tier 4 includes **all** of the following — not only editing package source:
|
|
|
36
36
|
- Files under `node_modules/@agent-native/*` (core, dispatch, scheduling, …)
|
|
37
37
|
- `pnpm.overrides`, `overrides`, `resolutions`, or `patchedDependencies` that
|
|
38
38
|
target any `@agent-native/*` package
|
|
39
|
-
-
|
|
40
|
-
|
|
39
|
+
- `pnpm patch`, `pnpm patch-commit`, or local package patch artifacts for any
|
|
40
|
+
dependency
|
|
41
|
+
- Invented "dispatch/core behavior" shims meant to paper over a version skew
|
|
42
|
+
or failed upgrade
|
|
41
43
|
|
|
42
44
|
This does not prohibit intentional app-owned UI customization. When public
|
|
43
45
|
props and composition are insufficient, the `customizing-agent-native` skill
|
|
@@ -100,8 +102,10 @@ inline strings in components.
|
|
|
100
102
|
- Don't confuse readable package source with app-owned code: use
|
|
101
103
|
`customizing-agent-native` and `agent-native eject` for supported ownership
|
|
102
104
|
transfer
|
|
105
|
+
- Don't use `pnpm patch` / `pnpm patch-commit`, commit `patches/` artifacts, or
|
|
106
|
+
add `pnpm.patchedDependencies` to "make the app run" after a version bump
|
|
103
107
|
- Don't add `pnpm.overrides` / `patchedDependencies` / `resolutions` for
|
|
104
|
-
`@agent-native/*` to
|
|
108
|
+
`@agent-native/*` to paper over a broken upgrade
|
|
105
109
|
- Don't invent local dispatch/core behavior overrides when upgrade fails —
|
|
106
110
|
run `npx @agent-native/core@latest upgrade`, then fix app-level errors only
|
|
107
111
|
- Don't modify `.agents/skills/` or `AGENTS.md` unless explicitly requested
|
|
@@ -15,8 +15,9 @@ metadata:
|
|
|
15
15
|
|
|
16
16
|
When an older Agent Native app/branch needs to run on current packages, use
|
|
17
17
|
`agent-native upgrade`. Never "fix" upgrade breakage with
|
|
18
|
-
`pnpm
|
|
19
|
-
|
|
18
|
+
`pnpm patch`, `pnpm patch-commit`, `pnpm.patchedDependencies`,
|
|
19
|
+
`pnpm.overrides`, `patchedDependencies`, `resolutions`, local patch artifacts,
|
|
20
|
+
or edits under `node_modules/@agent-native/*` — especially not against
|
|
20
21
|
`@agent-native/core` or `@agent-native/dispatch`.
|
|
21
22
|
|
|
22
23
|
## Why
|
|
@@ -109,6 +110,8 @@ install → refresh scaffold skills → verify, then fix **app** code only.
|
|
|
109
110
|
|
|
110
111
|
- Don't add `pnpm.overrides`, `overrides`, `resolutions`, or
|
|
111
112
|
`patchedDependencies` for any `@agent-native/*` package
|
|
113
|
+
- Don't run `pnpm patch` or `pnpm patch-commit`, or commit files under
|
|
114
|
+
`patches/`, for any dependency
|
|
112
115
|
- Don't edit `node_modules/@agent-native/core` or
|
|
113
116
|
`node_modules/@agent-native/dispatch`
|
|
114
117
|
- Don't invent local "dispatch behavior" shims to paper over version skew
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[build]
|
|
2
2
|
ignore = "node ./scripts/netlify-ignore-build.mjs tasks"
|
|
3
|
-
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && NITRO_PRESET=netlify pnpm --filter tasks build"
|
|
3
|
+
command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && NITRO_PRESET=netlify pnpm --filter tasks build && if [ \"${CONTEXT:-}\" = \"production\" ]; then pnpm --filter tasks migrate:production; fi"
|
|
4
4
|
publish = "templates/tasks/dist"
|
|
5
5
|
functions = "templates/tasks/.netlify/functions-internal"
|
|
6
6
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"start": "agent-native start",
|
|
11
11
|
"format.fix": "oxfmt --write .",
|
|
12
12
|
"typecheck": "agent-native typecheck",
|
|
13
|
+
"migrate:production": "tsx scripts/migrate-production.ts",
|
|
13
14
|
"action": "agent-native action",
|
|
14
15
|
"script": "agent-native script",
|
|
15
16
|
"db:push": "drizzle-kit push",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { closeDbExec, withMigrationRuntime } from "@agent-native/core/db";
|
|
2
|
+
import { runFrameworkReleaseMigrations } from "@agent-native/core/server";
|
|
3
|
+
|
|
4
|
+
import { runTasksMigrations } from "../server/plugins/db.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Release-time schema entrypoint for Tasks.
|
|
8
|
+
*
|
|
9
|
+
* This script is the production owner of schema changes. It runs against the
|
|
10
|
+
* direct migration endpoint selected by core, while request functions skip
|
|
11
|
+
* all migration and ensure-table work automatically.
|
|
12
|
+
*/
|
|
13
|
+
async function main(): Promise<void> {
|
|
14
|
+
await withMigrationRuntime(async () => {
|
|
15
|
+
await runFrameworkReleaseMigrations(null);
|
|
16
|
+
await runTasksMigrations(null);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await main();
|
|
22
|
+
} finally {
|
|
23
|
+
await closeDbExec();
|
|
24
|
+
}
|
|
@@ -8,7 +8,7 @@ import * as schema from "../db/schema.js";
|
|
|
8
8
|
|
|
9
9
|
// Keep historical entries unnamed for legacy-ledger compatibility; every new
|
|
10
10
|
// migration must add a stable, unique `name` slug.
|
|
11
|
-
const runTasksMigrations = runMigrations(
|
|
11
|
+
export const runTasksMigrations = runMigrations(
|
|
12
12
|
[
|
|
13
13
|
{
|
|
14
14
|
version: 1,
|
package/dist/action.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface ActionRunContext {
|
|
|
60
60
|
requestHeaders?: Headers;
|
|
61
61
|
/** Resolved org id, or `null` when the request has no org. */
|
|
62
62
|
orgId?: string | null;
|
|
63
|
+
/** Hosting app/template id used for app-owned resource boundaries. */
|
|
64
|
+
appId?: string;
|
|
63
65
|
/** How this action was invoked. */
|
|
64
66
|
caller: ActionCaller;
|
|
65
67
|
/** Present only for trigger-dispatched automation calls. */
|
|
@@ -697,6 +697,7 @@ export declare function runAgentLoop(opts: {
|
|
|
697
697
|
onOutcome?: (outcome: AgentLoopOutcome) => void;
|
|
698
698
|
ownerEmail?: string | null;
|
|
699
699
|
orgId?: string | null;
|
|
700
|
+
appId?: string;
|
|
700
701
|
/** Action invocation attribution. Defaults to the normal agent tool loop. */
|
|
701
702
|
actionCaller?: ActionCaller;
|
|
702
703
|
/** Trusted trigger lineage for automation-dispatched action calls. */
|
|
@@ -3974,6 +3974,7 @@ export async function runAgentLoop(opts) {
|
|
|
3974
3974
|
? Boolean(await actionEntry.needsApproval(toolCall.input, {
|
|
3975
3975
|
userEmail: getRequestUserEmail(),
|
|
3976
3976
|
orgId: getRequestOrgId() ?? null,
|
|
3977
|
+
appId: opts.appId,
|
|
3977
3978
|
caller: opts.actionCaller ?? "tool",
|
|
3978
3979
|
automation: opts.automation,
|
|
3979
3980
|
networkProtocol: opts.networkProtocol,
|
|
@@ -4370,6 +4371,7 @@ export async function runAgentLoop(opts) {
|
|
|
4370
4371
|
send,
|
|
4371
4372
|
userEmail: actionUserEmail ?? undefined,
|
|
4372
4373
|
orgId: actionOrgId,
|
|
4374
|
+
appId: opts.appId,
|
|
4373
4375
|
caller: opts.actionCaller ?? "tool",
|
|
4374
4376
|
automation: opts.automation,
|
|
4375
4377
|
networkProtocol: opts.networkProtocol,
|
|
@@ -17,6 +17,10 @@ interface ContentPart {
|
|
|
17
17
|
completedSideEffect?: boolean;
|
|
18
18
|
mcpApp?: AgentMcpAppPayload;
|
|
19
19
|
chatUI?: ActionChatUIConfig;
|
|
20
|
+
approval?: {
|
|
21
|
+
approvalKey: string;
|
|
22
|
+
dismissed?: boolean;
|
|
23
|
+
};
|
|
20
24
|
}
|
|
21
25
|
interface BuildAssistantMessageOptions {
|
|
22
26
|
suppressInternalContinuation?: boolean;
|
|
@@ -114,6 +114,14 @@ export function buildAssistantMessage(events, runId, options = {}) {
|
|
|
114
114
|
});
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
|
+
if (event.type === "approval_required") {
|
|
118
|
+
const matchingIndex = findApprovalToolCallIndex(content, event.tool ?? "unknown", event.toolCallId);
|
|
119
|
+
const part = content[matchingIndex];
|
|
120
|
+
if (part?.type === "tool-call") {
|
|
121
|
+
part.approval = { approvalKey: event.approvalKey };
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
117
125
|
if (event.type === "tool_done") {
|
|
118
126
|
const eventToolCallId = event.id?.trim();
|
|
119
127
|
let matchingIndex = -1;
|
|
@@ -339,6 +347,42 @@ function normalizeAttachmentIdentity(attachments) {
|
|
|
339
347
|
contentType: att?.contentType,
|
|
340
348
|
}));
|
|
341
349
|
}
|
|
350
|
+
function findApprovalToolCallIndex(content, toolName, toolCallId) {
|
|
351
|
+
if (toolCallId) {
|
|
352
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
353
|
+
const part = content[i];
|
|
354
|
+
if (part.type === "tool-call" &&
|
|
355
|
+
part.toolCallId === toolCallId &&
|
|
356
|
+
part.result === undefined) {
|
|
357
|
+
return i;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Older tool_start events without an id use one reader-local tc_N id.
|
|
361
|
+
// Only accept that fallback when it is unambiguous, so a replayed approval
|
|
362
|
+
// cannot attach its key to another same-name call.
|
|
363
|
+
const readerLocalCandidates = [];
|
|
364
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
365
|
+
const part = content[i];
|
|
366
|
+
if (part.type === "tool-call" &&
|
|
367
|
+
part.toolName === toolName &&
|
|
368
|
+
part.result === undefined &&
|
|
369
|
+
typeof part.toolCallId === "string" &&
|
|
370
|
+
/^tc_\d+$/.test(part.toolCallId)) {
|
|
371
|
+
readerLocalCandidates.push(i);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return readerLocalCandidates.length === 1 ? readerLocalCandidates[0] : -1;
|
|
375
|
+
}
|
|
376
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
377
|
+
const part = content[i];
|
|
378
|
+
if (part.type === "tool-call" &&
|
|
379
|
+
part.toolName === toolName &&
|
|
380
|
+
part.result === undefined) {
|
|
381
|
+
return i;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return -1;
|
|
385
|
+
}
|
|
342
386
|
// Strip the render-only `toolCallId` before fingerprinting. The id is generated
|
|
343
387
|
// differently depending on who built the message — the server now scopes it by
|
|
344
388
|
// run (`${runId}:tc_1`) while the client's live stream uses a bare counter
|
|
@@ -11,7 +11,11 @@ function normalizeActor(actor) {
|
|
|
11
11
|
const userEmail = actor.userEmail.trim().toLowerCase();
|
|
12
12
|
if (!userEmail)
|
|
13
13
|
throw httpError("Not authenticated.", 401);
|
|
14
|
-
return {
|
|
14
|
+
return {
|
|
15
|
+
userEmail,
|
|
16
|
+
orgId: actor.orgId?.trim() || null,
|
|
17
|
+
appId: actor.appId?.trim() || null,
|
|
18
|
+
};
|
|
15
19
|
}
|
|
16
20
|
function automationName(path) {
|
|
17
21
|
return path.replace(/^jobs\//, "").replace(/\.md$/, "");
|
|
@@ -56,6 +60,12 @@ async function requireOrganizationMembership(actor) {
|
|
|
56
60
|
function isOrganizationAdmin(membership) {
|
|
57
61
|
return membership.role === "owner" || membership.role === "admin";
|
|
58
62
|
}
|
|
63
|
+
function automationBelongsToApp(meta, appId) {
|
|
64
|
+
const ownerAppId = meta.appId?.trim();
|
|
65
|
+
if (!ownerAppId || !appId?.trim())
|
|
66
|
+
return true;
|
|
67
|
+
return ownerAppId === appId.trim();
|
|
68
|
+
}
|
|
59
69
|
async function mutationAccess(actorInput, resource, meta) {
|
|
60
70
|
const actor = normalizeActor(actorInput);
|
|
61
71
|
const resourceOrgId = organizationIdFromResourceOwner(resource.owner);
|
|
@@ -83,7 +93,10 @@ async function mutationAccess(actorInput, resource, meta) {
|
|
|
83
93
|
*/
|
|
84
94
|
export async function canUpdateAutomationResource(actorInput, resource) {
|
|
85
95
|
const { meta } = parseJobResource(resource.content);
|
|
86
|
-
|
|
96
|
+
const actor = normalizeActor(actorInput);
|
|
97
|
+
if (!automationBelongsToApp(meta, actor.appId))
|
|
98
|
+
return false;
|
|
99
|
+
return (await mutationAccess(actor, resource, meta)).canUpdate;
|
|
87
100
|
}
|
|
88
101
|
function assertExplicitAutomation(resource) {
|
|
89
102
|
const parsed = parseJobResource(resource.content);
|
|
@@ -111,6 +124,9 @@ async function readDefinition(actorInput, scope, name) {
|
|
|
111
124
|
throw httpError(`Automation "${automationName(path)}" not found.`, 404);
|
|
112
125
|
}
|
|
113
126
|
const definition = assertExplicitAutomation(resource);
|
|
127
|
+
if (!automationBelongsToApp(definition.meta, actor.appId)) {
|
|
128
|
+
throw httpError(`Automation "${automationName(path)}" not found.`, 404);
|
|
129
|
+
}
|
|
114
130
|
const access = await mutationAccess(actor, resource, definition.meta);
|
|
115
131
|
return {
|
|
116
132
|
...definition,
|
|
@@ -138,6 +154,8 @@ export async function listAutomationDefinitions(actorInput, scope) {
|
|
|
138
154
|
const parsed = parseJobResource(resource.content);
|
|
139
155
|
if (parsed.classification.kind !== "automation")
|
|
140
156
|
continue;
|
|
157
|
+
if (!automationBelongsToApp(parsed.meta, actor.appId))
|
|
158
|
+
continue;
|
|
141
159
|
const isCreator = parsed.meta.createdBy?.trim().toLowerCase() === actor.userEmail;
|
|
142
160
|
automations.push({
|
|
143
161
|
resource,
|
|
@@ -197,6 +215,7 @@ export async function defineAutomation(actorInput, input) {
|
|
|
197
215
|
condition: input.condition?.trim() || undefined,
|
|
198
216
|
mode: "agentic",
|
|
199
217
|
domain: input.domain?.trim() || undefined,
|
|
218
|
+
appId: actor.appId || undefined,
|
|
200
219
|
delegatedPolicyId: input.delegatedPolicyId?.trim() || undefined,
|
|
201
220
|
createdBy: actor.userEmail,
|
|
202
221
|
orgId: input.scope === "organization" ? actor.orgId : undefined,
|