amalgm 0.1.136 → 0.1.137
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/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/agent-bundles/bundles.js +41 -10
- package/runtime/scripts/amalgm-mcp/agent-bundles/rest.js +4 -3
- package/runtime/scripts/amalgm-mcp/events/desktop-release-runner.js +433 -6
- package/runtime/scripts/amalgm-mcp/tests/bundle-entries.test.js +105 -1
- package/runtime/scripts/amalgm-mcp/tests/desktop-release-runner.test.js +139 -0
package/package.json
CHANGED
|
@@ -216,6 +216,17 @@ function cleanEntryId(value) {
|
|
|
216
216
|
return typeof value === 'string' ? value.trim() : '';
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Tools shared as top-level primitives (not just dependencies of agents or
|
|
221
|
+
* automations). The bundle's explicit heads are the single source of truth.
|
|
222
|
+
*/
|
|
223
|
+
function standaloneToolIds(bundle) {
|
|
224
|
+
const heads = Array.isArray(bundle?.heads) ? bundle.heads : [];
|
|
225
|
+
return uniqueStrings(heads
|
|
226
|
+
.filter((head) => isObject(head) && cleanEntryId(head.type) === 'tool')
|
|
227
|
+
.map((head) => cleanEntryId(head.id)));
|
|
228
|
+
}
|
|
229
|
+
|
|
219
230
|
/**
|
|
220
231
|
* App elements carry the portable record and a file manifest; file contents
|
|
221
232
|
* stay in bundle.apps[] so the graph remains cheap to render.
|
|
@@ -353,7 +364,10 @@ function buildFlatAgentBundleGraph(bundle) {
|
|
|
353
364
|
}
|
|
354
365
|
}
|
|
355
366
|
|
|
356
|
-
|
|
367
|
+
// Every tool in bundle.tools gets an element — standalone tool heads are
|
|
368
|
+
// not referenced by any agent or automation edge but must still resolve.
|
|
369
|
+
const toolElementIds = new Set([...includedToolIds, ...toolMap.keys()]);
|
|
370
|
+
for (const toolId of toolElementIds) {
|
|
357
371
|
const tool = toolMap.get(toolId);
|
|
358
372
|
pushGraphElement(elements, {
|
|
359
373
|
id: graphElementId('tool', toolId),
|
|
@@ -370,7 +384,7 @@ function buildFlatAgentBundleGraph(bundle) {
|
|
|
370
384
|
}
|
|
371
385
|
|
|
372
386
|
for (const action of Array.isArray(bundle.toolActions) ? bundle.toolActions : []) {
|
|
373
|
-
if (!
|
|
387
|
+
if (!toolElementIds.has(action.toolId)) continue;
|
|
374
388
|
includedActionIds.add(action.id);
|
|
375
389
|
pushGraphElement(elements, {
|
|
376
390
|
id: graphElementId('tool_action', action.id),
|
|
@@ -529,7 +543,7 @@ function collectAgentGraph(rootAgentId) {
|
|
|
529
543
|
return ordered;
|
|
530
544
|
}
|
|
531
545
|
|
|
532
|
-
function bundleTitleAndDescription(agents, automations, apps) {
|
|
546
|
+
function bundleTitleAndDescription(agents, automations, apps, sharedTools) {
|
|
533
547
|
const firstAgent = agents[0]?.agent;
|
|
534
548
|
if (firstAgent) {
|
|
535
549
|
return { title: firstAgent.name || 'Shared agent', description: firstAgent.description || '' };
|
|
@@ -542,6 +556,10 @@ function bundleTitleAndDescription(agents, automations, apps) {
|
|
|
542
556
|
if (firstApp) {
|
|
543
557
|
return { title: firstApp.name || 'Shared app', description: firstApp.description || '' };
|
|
544
558
|
}
|
|
559
|
+
const firstTool = sharedTools?.[0];
|
|
560
|
+
if (firstTool) {
|
|
561
|
+
return { title: firstTool.name || 'Shared tool', description: firstTool.description || '' };
|
|
562
|
+
}
|
|
545
563
|
return { title: 'Shared bundle', description: '' };
|
|
546
564
|
}
|
|
547
565
|
|
|
@@ -566,8 +584,9 @@ function createBundle(input = {}, options = {}) {
|
|
|
566
584
|
const agentIds = uniqueStrings(input.agentIds);
|
|
567
585
|
const automationIds = uniqueStrings(input.automationIds);
|
|
568
586
|
const appIds = uniqueStrings(input.appIds);
|
|
569
|
-
|
|
570
|
-
|
|
587
|
+
const toolIds = uniqueStrings(input.toolIds);
|
|
588
|
+
if (agentIds.length === 0 && automationIds.length === 0 && appIds.length === 0 && toolIds.length === 0) {
|
|
589
|
+
throw new Error('At least one agent, automation, app, or tool is required');
|
|
571
590
|
}
|
|
572
591
|
|
|
573
592
|
const requires = {
|
|
@@ -614,7 +633,15 @@ function createBundle(input = {}, options = {}) {
|
|
|
614
633
|
skippedAppFiles += exported.skipped.length;
|
|
615
634
|
}
|
|
616
635
|
|
|
617
|
-
const { tools, toolActions } = collectToolRecords(loadoutToolIds, requires);
|
|
636
|
+
const { tools, toolActions } = collectToolRecords([...loadoutToolIds, ...toolIds], requires);
|
|
637
|
+
// Standalone tool heads must resolve to a real catalog tool — a dependency
|
|
638
|
+
// tool that is missing degrades to a placeholder, but a head cannot.
|
|
639
|
+
const sharedToolIds = toolIds.filter((toolId) => tools.some((tool) => tool.id === toolId));
|
|
640
|
+
const missingSharedToolIds = toolIds.filter((toolId) => !sharedToolIds.includes(toolId));
|
|
641
|
+
if (missingSharedToolIds.length > 0) {
|
|
642
|
+
throw new Error(`Tool not found: ${missingSharedToolIds.join(', ')}`);
|
|
643
|
+
}
|
|
644
|
+
const sharedTools = sharedToolIds.map((toolId) => tools.find((tool) => tool.id === toolId));
|
|
618
645
|
const skills = collectSkillRecords(agents);
|
|
619
646
|
const hooks = countHooks(agents);
|
|
620
647
|
const warnings = bundleWarnings({
|
|
@@ -624,15 +651,18 @@ function createBundle(input = {}, options = {}) {
|
|
|
624
651
|
apps: apps.length,
|
|
625
652
|
skippedAppFiles,
|
|
626
653
|
});
|
|
627
|
-
const { title, description } = bundleTitleAndDescription(agents, automations, apps);
|
|
654
|
+
const { title, description } = bundleTitleAndDescription(agents, automations, apps, sharedTools);
|
|
628
655
|
const heads = [
|
|
629
656
|
...agentIds.map((id) => ({ type: 'agent', id })),
|
|
630
657
|
...automationIds.map((id) => ({ type: 'automation', id })),
|
|
631
658
|
...appIds.map((id) => ({ type: 'app', id })),
|
|
659
|
+
...sharedToolIds.map((id) => ({ type: 'tool', id })),
|
|
632
660
|
];
|
|
633
661
|
|
|
634
662
|
const bundle = withBundleGraph({
|
|
635
|
-
kind: automations.length > 0 || apps.length > 0
|
|
663
|
+
kind: automations.length > 0 || apps.length > 0 || sharedToolIds.length > 0
|
|
664
|
+
? BUNDLE_KIND
|
|
665
|
+
: LEGACY_AGENT_BUNDLE_KIND,
|
|
636
666
|
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
637
667
|
bundleId: options.bundleId || bundleId(),
|
|
638
668
|
createdAt: nowIso(),
|
|
@@ -778,8 +808,9 @@ function validateBundle(bundle) {
|
|
|
778
808
|
const agents = Array.isArray(bundle.agents) ? bundle.agents : [];
|
|
779
809
|
const automations = Array.isArray(bundle.automations) ? bundle.automations : [];
|
|
780
810
|
const apps = Array.isArray(bundle.apps) ? bundle.apps : [];
|
|
781
|
-
|
|
782
|
-
|
|
811
|
+
const toolHeads = standaloneToolIds(bundle);
|
|
812
|
+
if (agents.length === 0 && automations.length === 0 && apps.length === 0 && toolHeads.length === 0) {
|
|
813
|
+
throw new Error('bundle must include at least one agent, automation, app, or tool');
|
|
783
814
|
}
|
|
784
815
|
if (agents.length > 0) assertClosedAgentBundleGraph(bundle);
|
|
785
816
|
return withBundleGraph(bundle);
|
|
@@ -22,12 +22,13 @@ async function handlePreview(body, sendJson) {
|
|
|
22
22
|
const agentIds = idList(body?.agent_id, body?.agent_ids);
|
|
23
23
|
const automationIds = idList(body?.automation_id, body?.automation_ids);
|
|
24
24
|
const appIds = idList(body?.app_id, body?.app_ids);
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const toolIds = idList(body?.tool_id, body?.tool_ids);
|
|
26
|
+
if (agentIds.length === 0 && automationIds.length === 0 && appIds.length === 0 && toolIds.length === 0) {
|
|
27
|
+
return sendJson(400, { error: 'At least one of agent_ids, automation_ids, app_ids, or tool_ids is required' });
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
try {
|
|
30
|
-
const result = createBundle({ agentIds, automationIds, appIds });
|
|
31
|
+
const result = createBundle({ agentIds, automationIds, appIds, toolIds });
|
|
31
32
|
return sendJson(200, { ok: true, ...result });
|
|
32
33
|
} catch (error) {
|
|
33
34
|
return sendJson(400, { error: error.message || 'Failed to create bundle' });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
@@ -19,11 +20,105 @@ const DEFAULT_DESKTOP_UPDATE_BASE_URL = 'https://amalgm-desktop-releases.fly.dev
|
|
|
19
20
|
const DEFAULT_DESKTOP_RELEASE_FLY_APP = 'amalgm-desktop-releases';
|
|
20
21
|
const DEFAULT_DESKTOP_RELEASE_FLY_ROOT = '/data';
|
|
21
22
|
const DEFAULT_DESKTOP_RELEASE_UPLOAD_CHUNK_BYTES = 2 * 1024 * 1024;
|
|
23
|
+
const DEFAULT_DESKTOP_NOTARY_SUBMISSION_DISCOVERY_TIMEOUT_MS = 2 * 60 * 1000;
|
|
24
|
+
const DEFAULT_DESKTOP_NOTARY_DECISION_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
25
|
+
const DEFAULT_DESKTOP_NOTARY_POLL_INTERVAL_MS = 15 * 1000;
|
|
22
26
|
|
|
23
27
|
function cleanString(value) {
|
|
24
28
|
return typeof value === 'string' ? value.trim() : '';
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
function booleanEnv(value, defaultValue = false) {
|
|
32
|
+
const raw = cleanString(value).toLowerCase();
|
|
33
|
+
if (!raw) return defaultValue;
|
|
34
|
+
if (['1', 'true', 'yes', 'on'].includes(raw)) return true;
|
|
35
|
+
if (['0', 'false', 'no', 'off'].includes(raw)) return false;
|
|
36
|
+
throw new Error(`Invalid boolean environment value: ${value}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function manualDesktopNotarizationEnabled(env = process.env) {
|
|
40
|
+
return booleanEnv(env.AMALGM_DESKTOP_RELEASE_MANUAL_NOTARIZE, true);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function codesignIdentityForCli(env = process.env) {
|
|
44
|
+
const raw = cleanString(
|
|
45
|
+
env.AMALGM_DESKTOP_CODESIGN_IDENTITY
|
|
46
|
+
|| env.CSC_NAME
|
|
47
|
+
|| DEFAULT_CSC_NAME,
|
|
48
|
+
).replace(/^Developer ID Application:\s*/i, '');
|
|
49
|
+
if (!raw) throw new Error('Missing macOS code signing identity for manual DMG signing.');
|
|
50
|
+
return `Developer ID Application: ${raw}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function appBuilderBinaryRelativePath(platform = process.platform, arch = process.arch) {
|
|
54
|
+
if (platform === 'darwin') {
|
|
55
|
+
return path.join('mac', arch === 'arm64' ? 'app-builder_arm64' : 'app-builder_amd64');
|
|
56
|
+
}
|
|
57
|
+
if (platform === 'linux') {
|
|
58
|
+
return path.join('linux', arch, 'app-builder');
|
|
59
|
+
}
|
|
60
|
+
if (platform === 'win32') {
|
|
61
|
+
if (arch === 'arm64') return path.join('win', 'arm64', 'app-builder.exe');
|
|
62
|
+
if (arch === 'ia32') return path.join('win', 'ia32', 'app-builder.exe');
|
|
63
|
+
return path.join('win', 'x64', 'app-builder.exe');
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Unsupported platform for app-builder binary lookup: ${platform}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function appBuilderBinaryPath(engineDir, env = process.env, platform = process.platform, arch = process.arch) {
|
|
69
|
+
const override = cleanString(env.AMALGM_DESKTOP_APP_BUILDER_BIN);
|
|
70
|
+
if (override) return override;
|
|
71
|
+
const binaryPath = path.join(
|
|
72
|
+
engineDir,
|
|
73
|
+
'node_modules',
|
|
74
|
+
'app-builder-bin',
|
|
75
|
+
appBuilderBinaryRelativePath(platform, arch),
|
|
76
|
+
);
|
|
77
|
+
if (!fs.existsSync(binaryPath)) {
|
|
78
|
+
throw new Error(`Missing app-builder binary for desktop blockmap generation: ${binaryPath}`);
|
|
79
|
+
}
|
|
80
|
+
return binaryPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function notarytoolCredentialArgs(env = process.env) {
|
|
84
|
+
const apiKey = cleanString(env.APPLE_API_KEY);
|
|
85
|
+
const apiKeyId = cleanString(env.APPLE_API_KEY_ID);
|
|
86
|
+
const apiIssuer = cleanString(env.APPLE_API_ISSUER);
|
|
87
|
+
if (apiKey || apiKeyId || apiIssuer) {
|
|
88
|
+
if (!apiKey || !apiKeyId || !apiIssuer) {
|
|
89
|
+
throw new Error('Incomplete Apple API key notarization credentials. Expected APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER.');
|
|
90
|
+
}
|
|
91
|
+
if (!fs.existsSync(apiKey)) throw new Error(`APPLE_API_KEY file does not exist: ${apiKey}`);
|
|
92
|
+
return {
|
|
93
|
+
args: ['--key', apiKey, '--key-id', apiKeyId, '--issuer', apiIssuer],
|
|
94
|
+
credentialType: 'api-key',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const appleId = cleanString(env.APPLE_ID);
|
|
99
|
+
const appPassword = cleanString(env.APPLE_APP_SPECIFIC_PASSWORD);
|
|
100
|
+
const teamId = cleanString(env.APPLE_TEAM_ID);
|
|
101
|
+
if (appleId || appPassword || teamId) {
|
|
102
|
+
if (!appleId || !appPassword || !teamId) {
|
|
103
|
+
throw new Error('Incomplete Apple ID notarization credentials. Expected APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID.');
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
args: ['--apple-id', appleId, '--password', appPassword, '--team-id', teamId],
|
|
107
|
+
credentialType: 'apple-id',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const keychainProfile = cleanString(env.APPLE_KEYCHAIN_PROFILE);
|
|
112
|
+
if (keychainProfile) {
|
|
113
|
+
return {
|
|
114
|
+
args: ['--keychain-profile', keychainProfile],
|
|
115
|
+
credentialType: 'keychain-profile',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error('Missing Apple notarization credentials for manual desktop release notarization.');
|
|
120
|
+
}
|
|
121
|
+
|
|
27
122
|
function parseRepoFullName(value) {
|
|
28
123
|
const raw = cleanString(value);
|
|
29
124
|
const match = /^([^/\s]+)\/([^/\s]+)$/.exec(raw);
|
|
@@ -348,7 +443,7 @@ function appendReleaseLog(filePath, text) {
|
|
|
348
443
|
fs.appendFileSync(logFile, text);
|
|
349
444
|
}
|
|
350
445
|
|
|
351
|
-
function
|
|
446
|
+
function runResult(command, args, options = {}) {
|
|
352
447
|
const printable = [command, ...args].join(' ');
|
|
353
448
|
const logFile = releaseLogFile(options);
|
|
354
449
|
appendReleaseLog(logFile, [
|
|
@@ -372,6 +467,12 @@ function run(command, args, options = {}) {
|
|
|
372
467
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
373
468
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
374
469
|
if (result.error) throw result.error;
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function run(command, args, options = {}) {
|
|
474
|
+
const printable = [command, ...args].join(' ');
|
|
475
|
+
const result = runResult(command, args, options);
|
|
375
476
|
if (result.status !== 0) {
|
|
376
477
|
throw new Error(`${printable} failed with exit code ${result.status ?? result.signal ?? 'unknown'}`);
|
|
377
478
|
}
|
|
@@ -582,7 +683,7 @@ function envForBuild(
|
|
|
582
683
|
...baseEnv,
|
|
583
684
|
DESKTOP_RELEASE_LANE: request.lane,
|
|
584
685
|
DESKTOP_RELEASE_CHANNEL: request.lane === 'preview' ? 'preview' : 'latest',
|
|
585
|
-
AMALGM_DESKTOP_THIN:
|
|
686
|
+
AMALGM_DESKTOP_THIN: desktopThinRuntimeBuildEnabled(baseEnv) ? '1' : '0',
|
|
586
687
|
AMALGM_RUNTIME_LABEL: request.lane === 'preview' ? 'preview' : 'main',
|
|
587
688
|
AMALGM_BRANCH: request.lane,
|
|
588
689
|
ELECTRON_VERSION: version,
|
|
@@ -597,6 +698,9 @@ function envForBuild(
|
|
|
597
698
|
AMALGM_DESKTOP_UPDATE_PROVIDER: publishTarget.updateProvider || normalizeUpdateProviderForRelease(publishTarget.provider),
|
|
598
699
|
CSC_NAME: baseEnv.CSC_NAME || DEFAULT_CSC_NAME,
|
|
599
700
|
};
|
|
701
|
+
if (manualDesktopNotarizationEnabled(baseEnv)) {
|
|
702
|
+
env.AMALGM_DESKTOP_NOTARIZE = 'off';
|
|
703
|
+
}
|
|
600
704
|
if (token) {
|
|
601
705
|
env.GH_TOKEN = token;
|
|
602
706
|
env.GITHUB_TOKEN = token;
|
|
@@ -625,6 +729,14 @@ function envForBuild(
|
|
|
625
729
|
return env;
|
|
626
730
|
}
|
|
627
731
|
|
|
732
|
+
function desktopThinRuntimeBuildEnabled(env = process.env) {
|
|
733
|
+
const raw = cleanString(env.AMALGM_DESKTOP_THIN || env.DESKTOP_THIN_BUILD).toLowerCase();
|
|
734
|
+
if (!raw) return true;
|
|
735
|
+
if (['1', 'true', 'yes', 'on'].includes(raw)) return true;
|
|
736
|
+
if (['0', 'false', 'no', 'off'].includes(raw)) return false;
|
|
737
|
+
throw new Error(`Unsupported AMALGM_DESKTOP_THIN value: ${raw}`);
|
|
738
|
+
}
|
|
739
|
+
|
|
628
740
|
function appOriginForLane(lane, env = process.env) {
|
|
629
741
|
if (lane === 'preview') {
|
|
630
742
|
return cleanString(env.AMALGM_DESKTOP_PREVIEW_APP_ORIGIN)
|
|
@@ -700,6 +812,295 @@ function desktopReleaseArtifactPaths(buildDir, lane, version) {
|
|
|
700
812
|
];
|
|
701
813
|
}
|
|
702
814
|
|
|
815
|
+
function desktopAppBundlePath(engineDir, lane) {
|
|
816
|
+
const appName = lane === 'preview' ? 'amalgm-preview.app' : 'amalgm.app';
|
|
817
|
+
return path.join(engineDir, 'build', 'mac-arm64', appName);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function releaseArtifactPath(artifactPaths, suffix) {
|
|
821
|
+
const match = artifactPaths.find((filePath) => (
|
|
822
|
+
filePath.endsWith(suffix)
|
|
823
|
+
&& !filePath.endsWith(`${suffix}.blockmap`)
|
|
824
|
+
));
|
|
825
|
+
if (!match) throw new Error(`Missing desktop release artifact with suffix ${suffix}`);
|
|
826
|
+
return match;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function parseJsonOutput(raw, label) {
|
|
830
|
+
try {
|
|
831
|
+
return JSON.parse(String(raw || ''));
|
|
832
|
+
} catch (error) {
|
|
833
|
+
throw new Error(`Failed to parse ${label} JSON: ${error.message}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function sha512Base64(filePath) {
|
|
838
|
+
const hash = crypto.createHash('sha512');
|
|
839
|
+
hash.update(fs.readFileSync(filePath));
|
|
840
|
+
return hash.digest('base64');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function updateChannelArtifactDigest(channelPath, filePath) {
|
|
844
|
+
if (!channelPath) return;
|
|
845
|
+
const raw = fs.readFileSync(channelPath, 'utf8');
|
|
846
|
+
const info = yaml.load(raw);
|
|
847
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) {
|
|
848
|
+
throw new Error(`Desktop release channel file is not a YAML object: ${channelPath}`);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const assetName = path.basename(filePath);
|
|
852
|
+
const size = fs.statSync(filePath).size;
|
|
853
|
+
const sha512 = sha512Base64(filePath);
|
|
854
|
+
if (Array.isArray(info.files)) {
|
|
855
|
+
for (const file of info.files) {
|
|
856
|
+
if (!file || typeof file !== 'object' || Array.isArray(file)) continue;
|
|
857
|
+
if (cleanString(file.url) !== assetName) continue;
|
|
858
|
+
file.size = size;
|
|
859
|
+
file.sha512 = sha512;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (cleanString(info.path) === assetName) {
|
|
863
|
+
info.sha512 = sha512;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
fs.writeFileSync(channelPath, yaml.dump(info, {
|
|
867
|
+
lineWidth: -1,
|
|
868
|
+
noRefs: true,
|
|
869
|
+
sortKeys: false,
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function regenerateArtifactBlockmap(engineDir, artifactPath, env = process.env) {
|
|
874
|
+
const blockmapPath = `${artifactPath}.blockmap`;
|
|
875
|
+
run(appBuilderBinaryPath(engineDir, env), [
|
|
876
|
+
'blockmap',
|
|
877
|
+
'--input',
|
|
878
|
+
artifactPath,
|
|
879
|
+
'--output',
|
|
880
|
+
blockmapPath,
|
|
881
|
+
], {
|
|
882
|
+
cwd: engineDir,
|
|
883
|
+
env,
|
|
884
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
885
|
+
});
|
|
886
|
+
return blockmapPath;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function recreateZipFromAppBundle(engineDir, appPath, zipPath, env = process.env) {
|
|
890
|
+
const tempZipPath = `${zipPath}.tmp-${process.pid}`;
|
|
891
|
+
fs.rmSync(tempZipPath, { force: true });
|
|
892
|
+
try {
|
|
893
|
+
run('ditto', [
|
|
894
|
+
'-c',
|
|
895
|
+
'-k',
|
|
896
|
+
'--sequesterRsrc',
|
|
897
|
+
'--keepParent',
|
|
898
|
+
appPath,
|
|
899
|
+
tempZipPath,
|
|
900
|
+
], {
|
|
901
|
+
cwd: engineDir,
|
|
902
|
+
env,
|
|
903
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
904
|
+
});
|
|
905
|
+
fs.renameSync(tempZipPath, zipPath);
|
|
906
|
+
return zipPath;
|
|
907
|
+
} catch (error) {
|
|
908
|
+
fs.rmSync(tempZipPath, { force: true });
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function parseNotarySubmissionId(payload) {
|
|
914
|
+
return cleanString(
|
|
915
|
+
payload?.id
|
|
916
|
+
|| payload?.submissionId
|
|
917
|
+
|| payload?.notarizationUpload?.id,
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function parseNotaryStatus(payload) {
|
|
922
|
+
return cleanString(
|
|
923
|
+
payload?.status
|
|
924
|
+
|| payload?.Status
|
|
925
|
+
|| payload?.notarizationUpload?.status,
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function notaryPollIntervalMs(env = process.env) {
|
|
930
|
+
return Math.max(1_000, Number(env.AMALGM_DESKTOP_NOTARY_POLL_INTERVAL_MS) || DEFAULT_DESKTOP_NOTARY_POLL_INTERVAL_MS);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function notarySubmissionDiscoveryTimeoutMs(env = process.env) {
|
|
934
|
+
return Math.max(5_000, Number(env.AMALGM_DESKTOP_NOTARY_SUBMISSION_DISCOVERY_TIMEOUT_MS) || DEFAULT_DESKTOP_NOTARY_SUBMISSION_DISCOVERY_TIMEOUT_MS);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function notaryDecisionTimeoutMs(env = process.env) {
|
|
938
|
+
return Math.max(30_000, Number(env.AMALGM_DESKTOP_NOTARY_DECISION_TIMEOUT_MS) || DEFAULT_DESKTOP_NOTARY_DECISION_TIMEOUT_MS);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function notaryHistoryEntries(cwd, env, authArgs) {
|
|
942
|
+
const stdout = run('xcrun', [
|
|
943
|
+
'notarytool',
|
|
944
|
+
'history',
|
|
945
|
+
...authArgs,
|
|
946
|
+
'--output-format',
|
|
947
|
+
'json',
|
|
948
|
+
], {
|
|
949
|
+
cwd,
|
|
950
|
+
env,
|
|
951
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
952
|
+
});
|
|
953
|
+
const parsed = parseJsonOutput(stdout, 'notarytool history');
|
|
954
|
+
if (Array.isArray(parsed?.history)) return parsed.history;
|
|
955
|
+
if (Array.isArray(parsed?.submissions)) return parsed.submissions;
|
|
956
|
+
if (Array.isArray(parsed)) return parsed;
|
|
957
|
+
return [];
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function waitForNotarySubmission(artifactName, startedAtMs, cwd, env, authArgs) {
|
|
961
|
+
const deadline = Date.now() + notarySubmissionDiscoveryTimeoutMs(env);
|
|
962
|
+
const threshold = startedAtMs - 60_000;
|
|
963
|
+
while (true) {
|
|
964
|
+
const entries = notaryHistoryEntries(cwd, env, authArgs)
|
|
965
|
+
.filter((entry) => cleanString(entry?.name) === artifactName)
|
|
966
|
+
.filter((entry) => {
|
|
967
|
+
const createdAt = Date.parse(cleanString(entry?.createdDate));
|
|
968
|
+
return !Number.isNaN(createdAt) && createdAt >= threshold;
|
|
969
|
+
})
|
|
970
|
+
.sort((left, right) => Date.parse(cleanString(right?.createdDate)) - Date.parse(cleanString(left?.createdDate)));
|
|
971
|
+
if (entries.length > 0) return entries[0];
|
|
972
|
+
if (Date.now() >= deadline) {
|
|
973
|
+
throw new Error(`Timed out waiting for notary submission to appear for ${artifactName}.`);
|
|
974
|
+
}
|
|
975
|
+
sleepSync(notaryPollIntervalMs(env));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function notaryInfo(submissionId, cwd, env, authArgs) {
|
|
980
|
+
const stdout = run('xcrun', [
|
|
981
|
+
'notarytool',
|
|
982
|
+
'info',
|
|
983
|
+
submissionId,
|
|
984
|
+
...authArgs,
|
|
985
|
+
'--output-format',
|
|
986
|
+
'json',
|
|
987
|
+
], {
|
|
988
|
+
cwd,
|
|
989
|
+
env,
|
|
990
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
991
|
+
});
|
|
992
|
+
return parseJsonOutput(stdout, 'notarytool info');
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function waitForNotaryDecision(submissionId, artifactName, cwd, env, authArgs) {
|
|
996
|
+
const deadline = Date.now() + notaryDecisionTimeoutMs(env);
|
|
997
|
+
let lastStatus = '';
|
|
998
|
+
while (true) {
|
|
999
|
+
const info = notaryInfo(submissionId, cwd, env, authArgs);
|
|
1000
|
+
const status = parseNotaryStatus(info);
|
|
1001
|
+
if (status && status !== 'In Progress') {
|
|
1002
|
+
if (status !== 'Accepted') {
|
|
1003
|
+
throw new Error(`Notary submission ${submissionId} for ${artifactName} finished with status ${status}.`);
|
|
1004
|
+
}
|
|
1005
|
+
return info;
|
|
1006
|
+
}
|
|
1007
|
+
lastStatus = status || lastStatus || 'In Progress';
|
|
1008
|
+
if (Date.now() >= deadline) {
|
|
1009
|
+
throw new Error(`Timed out waiting for notary submission ${submissionId} for ${artifactName}; last status ${lastStatus}.`);
|
|
1010
|
+
}
|
|
1011
|
+
sleepSync(notaryPollIntervalMs(env));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function submitArtifactForNotarization(filePath, cwd, env) {
|
|
1016
|
+
const { args: authArgs, credentialType } = notarytoolCredentialArgs(env);
|
|
1017
|
+
const startedAtMs = Date.now();
|
|
1018
|
+
const artifactName = path.basename(filePath);
|
|
1019
|
+
const result = runResult('xcrun', [
|
|
1020
|
+
'notarytool',
|
|
1021
|
+
'submit',
|
|
1022
|
+
filePath,
|
|
1023
|
+
...authArgs,
|
|
1024
|
+
'--no-progress',
|
|
1025
|
+
'--output-format',
|
|
1026
|
+
'json',
|
|
1027
|
+
], {
|
|
1028
|
+
cwd,
|
|
1029
|
+
env,
|
|
1030
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
let submissionId = '';
|
|
1034
|
+
if (cleanString(result.stdout)) {
|
|
1035
|
+
try {
|
|
1036
|
+
submissionId = parseNotarySubmissionId(parseJsonOutput(result.stdout, 'notarytool submit'));
|
|
1037
|
+
} catch {
|
|
1038
|
+
submissionId = '';
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (!submissionId) {
|
|
1042
|
+
submissionId = parseNotarySubmissionId(waitForNotarySubmission(artifactName, startedAtMs, cwd, env, authArgs));
|
|
1043
|
+
}
|
|
1044
|
+
if (!submissionId) {
|
|
1045
|
+
throw new Error(`Failed to resolve notary submission id for ${artifactName}.`);
|
|
1046
|
+
}
|
|
1047
|
+
if (result.status !== 0) {
|
|
1048
|
+
console.warn(`notarytool submit exited with code ${result.status} for ${artifactName}; continuing with submission ${submissionId}.`);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const info = waitForNotaryDecision(submissionId, artifactName, cwd, env, authArgs);
|
|
1052
|
+
return {
|
|
1053
|
+
id: submissionId,
|
|
1054
|
+
status: parseNotaryStatus(info),
|
|
1055
|
+
credentialType,
|
|
1056
|
+
info,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function manualNotarizeDesktopArtifacts({ engineDir, lane, env, artifactPaths, channelPath }) {
|
|
1061
|
+
const appPath = desktopAppBundlePath(engineDir, lane);
|
|
1062
|
+
const zipPath = releaseArtifactPath(artifactPaths, '.zip');
|
|
1063
|
+
const dmgPath = releaseArtifactPath(artifactPaths, '.dmg');
|
|
1064
|
+
|
|
1065
|
+
run('codesign', ['--verify', '--deep', '--strict', appPath], { cwd: engineDir, env });
|
|
1066
|
+
run('hdiutil', ['verify', dmgPath], { cwd: engineDir, env, maxBuffer: 10 * 1024 * 1024 });
|
|
1067
|
+
|
|
1068
|
+
run('codesign', [
|
|
1069
|
+
'--force',
|
|
1070
|
+
'--sign',
|
|
1071
|
+
codesignIdentityForCli(env),
|
|
1072
|
+
'--timestamp',
|
|
1073
|
+
dmgPath,
|
|
1074
|
+
], {
|
|
1075
|
+
cwd: engineDir,
|
|
1076
|
+
env,
|
|
1077
|
+
});
|
|
1078
|
+
run('hdiutil', ['verify', dmgPath], { cwd: engineDir, env, maxBuffer: 10 * 1024 * 1024 });
|
|
1079
|
+
const dmgNotarization = submitArtifactForNotarization(dmgPath, engineDir, env);
|
|
1080
|
+
run('xcrun', ['stapler', 'staple', appPath], { cwd: engineDir, env });
|
|
1081
|
+
run('xcrun', ['stapler', 'validate', appPath], { cwd: engineDir, env });
|
|
1082
|
+
run('spctl', ['--assess', '--type', 'execute', '--verbose=4', appPath], { cwd: engineDir, env });
|
|
1083
|
+
run('xcrun', ['stapler', 'staple', dmgPath], { cwd: engineDir, env });
|
|
1084
|
+
run('xcrun', ['stapler', 'validate', dmgPath], { cwd: engineDir, env });
|
|
1085
|
+
run('spctl', ['-a', '-vvv', '-t', 'install', dmgPath], { cwd: engineDir, env });
|
|
1086
|
+
const dmgBlockmapPath = regenerateArtifactBlockmap(engineDir, dmgPath, env);
|
|
1087
|
+
recreateZipFromAppBundle(engineDir, appPath, zipPath, env);
|
|
1088
|
+
const zipBlockmapPath = regenerateArtifactBlockmap(engineDir, zipPath, env);
|
|
1089
|
+
|
|
1090
|
+
updateChannelArtifactDigest(channelPath, zipPath);
|
|
1091
|
+
updateChannelArtifactDigest(channelPath, dmgPath);
|
|
1092
|
+
|
|
1093
|
+
return {
|
|
1094
|
+
enabled: true,
|
|
1095
|
+
appPath,
|
|
1096
|
+
zipPath,
|
|
1097
|
+
dmgPath,
|
|
1098
|
+
dmgBlockmapPath,
|
|
1099
|
+
zipBlockmapPath,
|
|
1100
|
+
dmgSubmissionId: dmgNotarization.id,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
703
1104
|
function githubReleaseId(tag, cwd, env, releaseRepository = desktopReleaseRepository(env)) {
|
|
704
1105
|
try {
|
|
705
1106
|
const publishedReleaseId = cleanString(run('gh', [
|
|
@@ -1261,6 +1662,7 @@ function publishDesktopRelease(request, options = {}) {
|
|
|
1261
1662
|
flyApp: publishTarget.flyApp || '',
|
|
1262
1663
|
flyRemoteDir: publishTarget.remoteDir || '',
|
|
1263
1664
|
releaseRepository: releaseRepository.fullName,
|
|
1665
|
+
manualNotarization: manualDesktopNotarizationEnabled(releaseEnv),
|
|
1264
1666
|
legacyBridge: desktopLegacyBridgePlan(releaseEnv, publishTarget),
|
|
1265
1667
|
};
|
|
1266
1668
|
}
|
|
@@ -1299,10 +1701,19 @@ function publishDesktopRelease(request, options = {}) {
|
|
|
1299
1701
|
run('git', ['push', 'origin', `refs/tags/v${version}`, '--force'], { cwd: engineDir });
|
|
1300
1702
|
run('npm', ['version', '--no-git-tag-version', '--allow-same-version', version], { cwd: engineDir, env: releaseEnv });
|
|
1301
1703
|
run('npm', ['run', 'compile'], { cwd: engineDir, env: releaseEnv });
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1704
|
+
const thinRuntimeBuild = desktopThinRuntimeBuildEnabled(releaseEnv);
|
|
1705
|
+
let appOrigin = appOriginForLane(request.lane, releaseEnv);
|
|
1706
|
+
if (thinRuntimeBuild) {
|
|
1707
|
+
if (!cleanString(releaseEnv.NEXT_PUBLIC_SUPABASE_URL)) {
|
|
1708
|
+
throw new Error('Missing NEXT_PUBLIC_SUPABASE_URL for thin desktop auth metadata.');
|
|
1709
|
+
}
|
|
1710
|
+
console.log('Building thin hosted-UI desktop shell; skipping bundled runtime and Next.js renderer payload.');
|
|
1711
|
+
} else {
|
|
1712
|
+
run('npm', ['run', 'npm:sync'], { cwd: engineDir, env: releaseEnv });
|
|
1713
|
+
copyUiIntoEngine(uiDir, engineDir);
|
|
1714
|
+
appOrigin = writeBundledWebEnv(engineDir, request.lane, releaseEnv);
|
|
1715
|
+
run('npm', ['run', 'build:next'], { cwd: engineDir, env: releaseEnv });
|
|
1716
|
+
}
|
|
1306
1717
|
if (request.lane === 'preview') run('npm', ['run', 'assets:preview-icon'], { cwd: engineDir, env: releaseEnv });
|
|
1307
1718
|
|
|
1308
1719
|
const buildEnv = envForBuild(releaseEnv, request, sourcePackageVersion, version, refs, shas, appOrigin, releaseRepository, publishTarget);
|
|
@@ -1319,6 +1730,15 @@ function publishDesktopRelease(request, options = {}) {
|
|
|
1319
1730
|
const tag = `v${version}`;
|
|
1320
1731
|
const artifacts = desktopReleaseArtifactPaths(path.join(engineDir, 'build'), request.lane, version);
|
|
1321
1732
|
const channelPath = artifacts.find((filePath) => filePath.endsWith('-mac.yml'));
|
|
1733
|
+
const manualNotarization = manualDesktopNotarizationEnabled(releaseEnv)
|
|
1734
|
+
? manualNotarizeDesktopArtifacts({
|
|
1735
|
+
engineDir,
|
|
1736
|
+
lane: request.lane,
|
|
1737
|
+
env: buildEnv,
|
|
1738
|
+
artifactPaths: artifacts,
|
|
1739
|
+
channelPath,
|
|
1740
|
+
})
|
|
1741
|
+
: { enabled: false };
|
|
1322
1742
|
let releaseUrl = '';
|
|
1323
1743
|
let legacyBridge = null;
|
|
1324
1744
|
if (publishTarget.provider === 'github') {
|
|
@@ -1358,6 +1778,7 @@ function publishDesktopRelease(request, options = {}) {
|
|
|
1358
1778
|
flyApp: publishTarget.flyApp || '',
|
|
1359
1779
|
flyRemoteDir: publishTarget.remoteDir || '',
|
|
1360
1780
|
releaseRepository: releaseRepository.fullName,
|
|
1781
|
+
manualNotarization,
|
|
1361
1782
|
legacyBridge,
|
|
1362
1783
|
artifacts: artifacts.map((filePath) => path.basename(filePath)),
|
|
1363
1784
|
repository: request.repository,
|
|
@@ -1387,7 +1808,10 @@ if (require.main === module) {
|
|
|
1387
1808
|
module.exports = {
|
|
1388
1809
|
absoluteAssetUrl,
|
|
1389
1810
|
applyDesktopReleaseEnv,
|
|
1811
|
+
appBuilderBinaryPath,
|
|
1812
|
+
appBuilderBinaryRelativePath,
|
|
1390
1813
|
bridgeChannelInfoForBase,
|
|
1814
|
+
codesignIdentityForCli,
|
|
1391
1815
|
buildNumberFromNow,
|
|
1392
1816
|
desktopLegacyBridgeEnabled,
|
|
1393
1817
|
desktopLegacyBridgePlan,
|
|
@@ -1400,10 +1824,13 @@ module.exports = {
|
|
|
1400
1824
|
desktopVersion,
|
|
1401
1825
|
desktopReleaseArtifactPaths,
|
|
1402
1826
|
envForBuild,
|
|
1827
|
+
manualDesktopNotarizationEnabled,
|
|
1828
|
+
notarytoolCredentialArgs,
|
|
1403
1829
|
parseEnvFile,
|
|
1404
1830
|
parseUploadOffsetMismatch,
|
|
1405
1831
|
publishLegacyGithubBridgeRelease,
|
|
1406
1832
|
publishDesktopRelease,
|
|
1833
|
+
recreateZipFromAppBundle,
|
|
1407
1834
|
recoverableUploadOffset,
|
|
1408
1835
|
requestFromEnv,
|
|
1409
1836
|
shouldHandleRequest,
|
|
@@ -239,7 +239,7 @@ test('mixed bundle carries automations and apps with heads and installs everythi
|
|
|
239
239
|
test('legacy agent-only bundles still validate and empty bundles are rejected', () => {
|
|
240
240
|
assert.throws(
|
|
241
241
|
() => validateBundle({ kind: 'amalgm.bundle', schemaVersion: 1 }),
|
|
242
|
-
/at least one agent, automation, or
|
|
242
|
+
/at least one agent, automation, app, or tool/,
|
|
243
243
|
);
|
|
244
244
|
assert.throws(
|
|
245
245
|
() => validateBundle({ kind: 'something.else', schemaVersion: 1 }),
|
|
@@ -247,6 +247,110 @@ test('legacy agent-only bundles still validate and empty bundles are rejected',
|
|
|
247
247
|
);
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
+
test('standalone tool bundle carries the tool as a head and installs it', async () => {
|
|
251
|
+
const { defaultToolboxService } = require('../toolbox/service');
|
|
252
|
+
defaultToolboxService.registerTool({
|
|
253
|
+
id: 'acme.standalone',
|
|
254
|
+
name: 'Acme Standalone',
|
|
255
|
+
description: 'A standalone shareable tool',
|
|
256
|
+
type: 'cli',
|
|
257
|
+
origin: 'user',
|
|
258
|
+
source: { command: 'acme-standalone', inputMode: 'json-stdin', outputMode: 'json' },
|
|
259
|
+
actions: [{
|
|
260
|
+
id: 'acme.standalone.run',
|
|
261
|
+
name: 'run',
|
|
262
|
+
description: 'Run it.',
|
|
263
|
+
inputSchema: { type: 'object' },
|
|
264
|
+
}],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const { bundle, preview } = createBundle({ toolIds: ['acme.standalone'] });
|
|
268
|
+
|
|
269
|
+
assert.equal(bundle.kind, 'amalgm.bundle');
|
|
270
|
+
assert.equal(bundle.title, 'Acme Standalone');
|
|
271
|
+
assert.deepEqual(bundle.heads, [{ type: 'tool', id: 'acme.standalone' }]);
|
|
272
|
+
assert.deepEqual(bundle.headIds, ['tool:acme.standalone']);
|
|
273
|
+
assert.equal(bundle.tools.length, 1);
|
|
274
|
+
assert.equal(bundle.toolActions.length, 1);
|
|
275
|
+
const toolElement = bundle.elements.find((element) => element.id === 'tool:acme.standalone');
|
|
276
|
+
assert.equal(toolElement.label, 'Acme Standalone');
|
|
277
|
+
const actionElement = bundle.elements.find((element) => element.type === 'tool_action');
|
|
278
|
+
assert.equal(actionElement.data.id, 'acme.standalone.run');
|
|
279
|
+
assert.equal(preview.tools.length, 1);
|
|
280
|
+
|
|
281
|
+
const revived = validateBundle(JSON.parse(JSON.stringify(bundle)));
|
|
282
|
+
assert.deepEqual(revived.headIds, ['tool:acme.standalone']);
|
|
283
|
+
|
|
284
|
+
defaultToolboxService.deleteTool('acme.standalone');
|
|
285
|
+
const result = await installBundle(JSON.parse(JSON.stringify(bundle)));
|
|
286
|
+
assert.equal(result.ok, true);
|
|
287
|
+
assert.equal(result.installedTools.length, 1);
|
|
288
|
+
assert.equal(result.installedTools[0].id, 'acme.standalone');
|
|
289
|
+
const catalog = defaultToolboxService.readCatalog();
|
|
290
|
+
assert.equal(catalog.tools['acme.standalone'].name, 'Acme Standalone');
|
|
291
|
+
assert.equal(Boolean(catalog.toolActions['acme.standalone.run']), true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('unknown standalone tool ids are rejected at export', () => {
|
|
295
|
+
assert.throws(
|
|
296
|
+
() => createBundle({ toolIds: ['does.not.exist'] }),
|
|
297
|
+
/Tool not found: does\.not\.exist/,
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('tools shared by multiple agents and standalone stay canonical (one record per id)', () => {
|
|
302
|
+
const { createAgent } = require('../agents/store');
|
|
303
|
+
const { upsertAgentConfig } = require('../agent-config/store');
|
|
304
|
+
const { defaultToolboxService } = require('../toolbox/service');
|
|
305
|
+
|
|
306
|
+
defaultToolboxService.registerTool({
|
|
307
|
+
id: 'acme.shared',
|
|
308
|
+
name: 'Acme Shared',
|
|
309
|
+
type: 'cli',
|
|
310
|
+
origin: 'user',
|
|
311
|
+
source: { command: 'acme-shared', inputMode: 'json-stdin', outputMode: 'json' },
|
|
312
|
+
actions: [{ id: 'acme.shared.run', name: 'run', inputSchema: { type: 'object' } }],
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const left = createAgent({
|
|
316
|
+
id: 'dedupe-left',
|
|
317
|
+
name: 'Dedupe Left',
|
|
318
|
+
baseHarnessId: 'codex',
|
|
319
|
+
loadout: { toolIds: ['acme.shared'] },
|
|
320
|
+
authMethod: 'amalgm',
|
|
321
|
+
});
|
|
322
|
+
upsertAgentConfig(left.id, { instructions: 'left', loadout: { toolIds: ['acme.shared'] } });
|
|
323
|
+
const right = createAgent({
|
|
324
|
+
id: 'dedupe-right',
|
|
325
|
+
name: 'Dedupe Right',
|
|
326
|
+
baseHarnessId: 'codex',
|
|
327
|
+
loadout: { toolIds: ['acme.shared'] },
|
|
328
|
+
authMethod: 'amalgm',
|
|
329
|
+
});
|
|
330
|
+
upsertAgentConfig(right.id, { instructions: 'right', loadout: { toolIds: ['acme.shared'] } });
|
|
331
|
+
|
|
332
|
+
const { bundle } = createBundle({
|
|
333
|
+
agentIds: [left.id, right.id],
|
|
334
|
+
toolIds: ['acme.shared'],
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// One canonical tool record and element; both agents point edges at it.
|
|
338
|
+
assert.equal(bundle.tools.filter((tool) => tool.id === 'acme.shared').length, 1);
|
|
339
|
+
assert.equal(
|
|
340
|
+
bundle.elements.filter((element) => element.id === 'tool:acme.shared').length,
|
|
341
|
+
1,
|
|
342
|
+
);
|
|
343
|
+
const usesToolEdges = bundle.edges.filter(
|
|
344
|
+
(edge) => edge.to === 'tool:acme.shared' && edge.kind === 'uses-tool',
|
|
345
|
+
);
|
|
346
|
+
assert.equal(usesToolEdges.length, 2);
|
|
347
|
+
assert.deepEqual(bundle.heads, [
|
|
348
|
+
{ type: 'agent', id: left.id },
|
|
349
|
+
{ type: 'agent', id: right.id },
|
|
350
|
+
{ type: 'tool', id: 'acme.shared' },
|
|
351
|
+
]);
|
|
352
|
+
});
|
|
353
|
+
|
|
250
354
|
test('rest handlers round-trip a share: preview exports a bundle and install recreates it', async () => {
|
|
251
355
|
const { handlePreview, handleInstall } = require('../agent-bundles/rest');
|
|
252
356
|
const automation = createAutomation({
|
|
@@ -5,12 +5,16 @@ const assert = require('node:assert/strict');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
8
9
|
const yaml = require('js-yaml');
|
|
9
10
|
|
|
10
11
|
const {
|
|
11
12
|
absoluteAssetUrl,
|
|
12
13
|
applyDesktopReleaseEnv,
|
|
14
|
+
appBuilderBinaryPath,
|
|
15
|
+
appBuilderBinaryRelativePath,
|
|
13
16
|
bridgeChannelInfoForBase,
|
|
17
|
+
codesignIdentityForCli,
|
|
14
18
|
desktopLegacyBridgePlan,
|
|
15
19
|
desktopLegacyBridgeRepository,
|
|
16
20
|
desktopReleaseArtifactPaths,
|
|
@@ -21,9 +25,12 @@ const {
|
|
|
21
25
|
desktopUpdateUrlForLane,
|
|
22
26
|
desktopVersion,
|
|
23
27
|
envForBuild,
|
|
28
|
+
manualDesktopNotarizationEnabled,
|
|
29
|
+
notarytoolCredentialArgs,
|
|
24
30
|
parseEnvFile,
|
|
25
31
|
parseUploadOffsetMismatch,
|
|
26
32
|
publishDesktopRelease,
|
|
33
|
+
recreateZipFromAppBundle,
|
|
27
34
|
recoverableUploadOffset,
|
|
28
35
|
requestFromEnv,
|
|
29
36
|
shouldHandleRequest,
|
|
@@ -205,6 +212,24 @@ test('desktop release runner uses electron-builder compatible signing identity n
|
|
|
205
212
|
assert.equal(env.AMALGM_DESKTOP_RELEASE_FLY_APP, 'amalgm-desktop-releases');
|
|
206
213
|
assert.equal(env.AMALGM_DESKTOP_RELEASE_FLY_ROOT, '/data');
|
|
207
214
|
assert.equal(env.AMALGM_DESKTOP_THIN, '1');
|
|
215
|
+
assert.equal(env.AMALGM_DESKTOP_NOTARIZE, 'off');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('desktop release runner preserves explicit bundled desktop builds', () => {
|
|
219
|
+
const env = envForBuild(
|
|
220
|
+
{
|
|
221
|
+
GH_TOKEN: 'token',
|
|
222
|
+
AMALGM_DESKTOP_THIN: '0',
|
|
223
|
+
},
|
|
224
|
+
{ lane: 'main', delivery: 'delivery-id' },
|
|
225
|
+
'0.1.124',
|
|
226
|
+
'0.1.124000001',
|
|
227
|
+
{ engineRef: 'engine', uiRef: 'ui' },
|
|
228
|
+
{ engineSha: 'engine-sha', uiSha: 'ui-sha' },
|
|
229
|
+
'https://amalgm.ai',
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
assert.equal(env.AMALGM_DESKTOP_THIN, '0');
|
|
208
233
|
});
|
|
209
234
|
|
|
210
235
|
test('desktop release runner omits blank certificate file secrets', () => {
|
|
@@ -226,6 +251,120 @@ test('desktop release runner omits blank certificate file secrets', () => {
|
|
|
226
251
|
assert.equal(env.CSC_NAME, 'amalgm, Inc. (C5S6UATV3L)');
|
|
227
252
|
});
|
|
228
253
|
|
|
254
|
+
test('desktop release runner manual notarization can be disabled explicitly', () => {
|
|
255
|
+
assert.equal(manualDesktopNotarizationEnabled({}), true);
|
|
256
|
+
assert.equal(manualDesktopNotarizationEnabled({
|
|
257
|
+
AMALGM_DESKTOP_RELEASE_MANUAL_NOTARIZE: '0',
|
|
258
|
+
}), false);
|
|
259
|
+
|
|
260
|
+
const env = envForBuild(
|
|
261
|
+
{
|
|
262
|
+
GH_TOKEN: 'token',
|
|
263
|
+
AMALGM_DESKTOP_RELEASE_MANUAL_NOTARIZE: '0',
|
|
264
|
+
AMALGM_DESKTOP_NOTARIZE: 'required',
|
|
265
|
+
},
|
|
266
|
+
{ lane: 'main', delivery: 'delivery-id' },
|
|
267
|
+
'0.1.124',
|
|
268
|
+
'0.1.124000001',
|
|
269
|
+
{ engineRef: 'engine', uiRef: 'ui' },
|
|
270
|
+
{ engineSha: 'engine-sha', uiSha: 'ui-sha' },
|
|
271
|
+
'https://amalgm.ai',
|
|
272
|
+
);
|
|
273
|
+
assert.equal(env.AMALGM_DESKTOP_NOTARIZE, 'required');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('desktop release runner resolves manual notarization credentials and CLI identity', () => {
|
|
277
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-desktop-release-notary-'));
|
|
278
|
+
const keyPath = path.join(tempRoot, 'AuthKey_TEST.p8');
|
|
279
|
+
fs.writeFileSync(keyPath, 'dummy-key');
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
assert.deepEqual(notarytoolCredentialArgs({
|
|
283
|
+
APPLE_API_KEY: keyPath,
|
|
284
|
+
APPLE_API_KEY_ID: 'KEYID123',
|
|
285
|
+
APPLE_API_ISSUER: 'issuer-123',
|
|
286
|
+
}), {
|
|
287
|
+
args: ['--key', keyPath, '--key-id', 'KEYID123', '--issuer', 'issuer-123'],
|
|
288
|
+
credentialType: 'api-key',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
assert.deepEqual(notarytoolCredentialArgs({
|
|
292
|
+
APPLE_ID: 'user@example.com',
|
|
293
|
+
APPLE_APP_SPECIFIC_PASSWORD: 'app-password',
|
|
294
|
+
APPLE_TEAM_ID: 'TEAM123',
|
|
295
|
+
}), {
|
|
296
|
+
args: ['--apple-id', 'user@example.com', '--password', 'app-password', '--team-id', 'TEAM123'],
|
|
297
|
+
credentialType: 'apple-id',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
assert.equal(
|
|
301
|
+
codesignIdentityForCli({ CSC_NAME: 'amalgm, Inc. (C5S6UATV3L)' }),
|
|
302
|
+
'Developer ID Application: amalgm, Inc. (C5S6UATV3L)',
|
|
303
|
+
);
|
|
304
|
+
assert.equal(
|
|
305
|
+
codesignIdentityForCli({ CSC_NAME: 'Developer ID Application: amalgm, Inc. (C5S6UATV3L)' }),
|
|
306
|
+
'Developer ID Application: amalgm, Inc. (C5S6UATV3L)',
|
|
307
|
+
);
|
|
308
|
+
assert.equal(
|
|
309
|
+
appBuilderBinaryRelativePath('darwin', 'arm64'),
|
|
310
|
+
path.join('mac', 'app-builder_arm64'),
|
|
311
|
+
);
|
|
312
|
+
assert.equal(
|
|
313
|
+
appBuilderBinaryRelativePath('darwin', 'x64'),
|
|
314
|
+
path.join('mac', 'app-builder_amd64'),
|
|
315
|
+
);
|
|
316
|
+
assert.equal(
|
|
317
|
+
appBuilderBinaryRelativePath('linux', 'arm64'),
|
|
318
|
+
path.join('linux', 'arm64', 'app-builder'),
|
|
319
|
+
);
|
|
320
|
+
} finally {
|
|
321
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('desktop release runner resolves app-builder binary path from checkout node_modules or override', () => {
|
|
326
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-desktop-release-app-builder-'));
|
|
327
|
+
const engineDir = path.join(tempRoot, 'engine');
|
|
328
|
+
const binaryPath = path.join(engineDir, 'node_modules', 'app-builder-bin', 'mac', 'app-builder_arm64');
|
|
329
|
+
fs.mkdirSync(path.dirname(binaryPath), { recursive: true });
|
|
330
|
+
fs.writeFileSync(binaryPath, '');
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
assert.equal(
|
|
334
|
+
appBuilderBinaryPath(engineDir, {}, 'darwin', 'arm64'),
|
|
335
|
+
binaryPath,
|
|
336
|
+
);
|
|
337
|
+
assert.equal(
|
|
338
|
+
appBuilderBinaryPath(engineDir, { AMALGM_DESKTOP_APP_BUILDER_BIN: '/tmp/custom-app-builder' }, 'darwin', 'arm64'),
|
|
339
|
+
'/tmp/custom-app-builder',
|
|
340
|
+
);
|
|
341
|
+
} finally {
|
|
342
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('desktop release runner recreates updater zip from app bundle', () => {
|
|
347
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-desktop-release-zip-'));
|
|
348
|
+
const engineDir = path.join(tempRoot, 'engine');
|
|
349
|
+
const appPath = path.join(engineDir, 'build', 'mac-arm64', 'amalgm-preview.app');
|
|
350
|
+
const markerPath = path.join(appPath, 'Contents', 'Resources', 'marker.txt');
|
|
351
|
+
const zipPath = path.join(engineDir, 'build', 'amalgm-preview-0.1.1-arm64.zip');
|
|
352
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
353
|
+
fs.writeFileSync(markerPath, 'fresh app payload');
|
|
354
|
+
fs.writeFileSync(zipPath, 'old zip payload');
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
recreateZipFromAppBundle(engineDir, appPath, zipPath, {});
|
|
358
|
+
assert.ok(fs.statSync(zipPath).size > 'old zip payload'.length);
|
|
359
|
+
|
|
360
|
+
const listing = spawnSync('unzip', ['-l', zipPath], { encoding: 'utf8' });
|
|
361
|
+
assert.equal(listing.status, 0, listing.stderr);
|
|
362
|
+
assert.match(listing.stdout, /amalgm-preview\.app\/Contents\/Resources\/marker\.txt/);
|
|
363
|
+
} finally {
|
|
364
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
229
368
|
test('desktop release repository can be overridden as owner/repo or full name', () => {
|
|
230
369
|
assert.deepEqual(desktopReleaseRepository({
|
|
231
370
|
AMALGM_DESKTOP_UPDATE_OWNER: 'example',
|