@indigoai-us/hq-cli 5.341.1 → 5.341.3
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/CHANGELOG.md +17 -0
- package/assets/hqd/windows-pipe-host.ps1 +282 -0
- package/dist/bin/hq-auth-refresh.d.ts +1 -1
- package/dist/bin/hq-auth-refresh.js +11 -2
- package/dist/commands/billing-sponsor.js +20 -1
- package/dist/commands/core-derive-trigger-facts-native.d.ts +2 -0
- package/dist/commands/core-derive-trigger-facts-native.js +92 -25
- package/dist/commands/core-migrate-policy-triggers-native.d.ts +1 -0
- package/dist/commands/core-migrate-policy-triggers-native.js +26 -12
- package/dist/commands/doctor.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +17 -1
- package/dist/lib/aws-sdk-node-warning-output.js +2 -3
- package/dist/lib/billing/plan-lock.js +19 -2
- package/dist/lib/bot/owner-context.d.ts +2 -0
- package/dist/lib/bot/owner-context.js +19 -1
- package/dist/lib/daemon/flag-monitor.d.ts +23 -0
- package/dist/lib/daemon/flag-monitor.js +99 -0
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +10 -1
- package/dist/lib/daemon/protocol.d.ts +26 -3
- package/dist/lib/daemon/protocol.js +9 -5
- package/dist/lib/daemon/run.js +14 -16
- package/dist/lib/daemon/server.d.ts +23 -2
- package/dist/lib/daemon/server.js +397 -52
- package/dist/lib/daemon/windows-pipe-host.d.ts +17 -0
- package/dist/lib/daemon/windows-pipe-host.js +218 -0
- package/dist/lib/diagnostics.d.ts +9 -0
- package/dist/lib/diagnostics.js +12 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +6 -1
- package/dist/lib/doctor/checks/anywhere.js +131 -13
- package/dist/lib/doctor/json-output.d.ts +2 -0
- package/dist/lib/doctor/json-output.js +1 -0
- package/dist/lib/doctor/registry.js +1 -3
- package/dist/lib/doctor/types.d.ts +4 -0
- package/dist/utils/package-use-lease.d.ts +4 -0
- package/dist/utils/package-use-lease.js +28 -0
- package/dist/utils/run-bundled-script.d.ts +2 -0
- package/dist/utils/run-bundled-script.js +10 -1
- package/dist/utils/vault-api.js +12 -12
- package/docs/anywhere-runtime.md +49 -5
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.341.3] — 2026-10-03
|
|
6
|
+
- Preserve policy-trigger parity for Windows PATH resolution, filesystem paths, and frontmatter parsing.
|
|
7
|
+
- Report unexpected company-resolve, plan-limit body and late owner-context failures that a bare catch used to hide. The report prints only with `--verbose` or `HQ_DEBUG` set.
|
|
8
|
+
- Enable hqd on Windows through a bundled PowerShell named-pipe host that applies and verifies a current-user-only DACL, while refusing startup if the security check fails.
|
|
9
|
+
- `hq doctor` now reports hqd reachability, runtime flag freshness and fallback
|
|
10
|
+
reasons for the current directory. The same fields are available with
|
|
11
|
+
`hq doctor --json`, including while the runtime flag is off.
|
|
12
|
+
- Hardened Unix hqd socket startup to reject symlinks anywhere in the socket directory path and unexpected socket-path entries, replace only a stale socket owned by the current user, and verify user-only directory and socket permissions. The default socket now lives under the private `~/.hq/daemon/` directory.
|
|
13
|
+
|
|
14
|
+
## [5.341.2] — 2026-10-03
|
|
15
|
+
|
|
16
|
+
- Preserve the original integrations response read error instead of returning a consumed response.
|
|
17
|
+
|
|
18
|
+
- The CLI no longer starts while a confirmed Desktop update is in progress after a lease I/O error.
|
|
19
|
+
- hqd refreshes `hq-anywhere-runtime` every 30 seconds, fails closed when the last successful read is older than 60 seconds, and clears its in-memory stdin login when the flag turns off.
|
|
20
|
+
- Hardened hqd company resolution so unreadable registries and registry links to companies missing from the configured HQ manifest fall back to personal context. Added adversarial coverage for symlinks, nested repositories, clones, path normalization, worktrees, and session bindings.
|
|
21
|
+
|
|
5
22
|
## [5.341.1] — 2026-10-03
|
|
6
23
|
- Match native policy frontmatter and report bytes to the bundled shell on Windows, including paths, diagnostics, and JSON newlines.
|
|
7
24
|
- Keep session-journal printable paths byte-compatible with the shell on Windows.
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
[Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
3
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
4
|
+
try {
|
|
5
|
+
Add-Type -AssemblyName System.IO.Pipes.AccessControl -ErrorAction SilentlyContinue
|
|
6
|
+
} catch {
|
|
7
|
+
# Windows PowerShell 5.1 ships the access-control types in its framework assemblies.
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
$source = @'
|
|
11
|
+
using System;
|
|
12
|
+
using System.Collections;
|
|
13
|
+
using System.Collections.Concurrent;
|
|
14
|
+
using System.IO;
|
|
15
|
+
using System.IO.Pipes;
|
|
16
|
+
using System.Linq;
|
|
17
|
+
using System.Reflection;
|
|
18
|
+
using System.Security.AccessControl;
|
|
19
|
+
using System.Security.Principal;
|
|
20
|
+
using System.Text;
|
|
21
|
+
using System.Threading;
|
|
22
|
+
using System.Threading.Tasks;
|
|
23
|
+
using Microsoft.Win32.SafeHandles;
|
|
24
|
+
using System.Runtime.InteropServices;
|
|
25
|
+
|
|
26
|
+
public static class HqdWindowsPipeHost
|
|
27
|
+
{
|
|
28
|
+
private static readonly object OutputLock = new object();
|
|
29
|
+
private static readonly ConcurrentDictionary<int, NamedPipeServerStream> Connections = new ConcurrentDictionary<int, NamedPipeServerStream>();
|
|
30
|
+
private static int NextConnectionId;
|
|
31
|
+
private static volatile bool Stopping;
|
|
32
|
+
private static readonly ManualResetEventSlim AcceptStarted = new ManualResetEventSlim(false);
|
|
33
|
+
private static string PipeName;
|
|
34
|
+
private static SecurityIdentifier UserSid;
|
|
35
|
+
private static PipeSecurity Security;
|
|
36
|
+
|
|
37
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
38
|
+
private static extern bool GetNamedPipeClientComputerName(SafePipeHandle pipe, StringBuilder computerName, uint length);
|
|
39
|
+
|
|
40
|
+
private static void Emit(string frame)
|
|
41
|
+
{
|
|
42
|
+
lock (OutputLock)
|
|
43
|
+
{
|
|
44
|
+
Console.Out.WriteLine(frame);
|
|
45
|
+
Console.Out.Flush();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private static string B64(string value)
|
|
50
|
+
{
|
|
51
|
+
return Convert.ToBase64String(Encoding.UTF8.GetBytes(value));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private static void EmitFatal(Exception error)
|
|
55
|
+
{
|
|
56
|
+
Emit("FATAL\t" + B64(error.GetBaseException().Message));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static PipeSecurity BuildSecurity(SecurityIdentifier sid)
|
|
60
|
+
{
|
|
61
|
+
PipeSecurity security = new PipeSecurity();
|
|
62
|
+
security.SetAccessRuleProtection(true, false);
|
|
63
|
+
security.AddAccessRule(new PipeAccessRule(sid, PipeAccessRights.FullControl, AccessControlType.Allow));
|
|
64
|
+
return security;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static void VerifySecurity(NamedPipeServerStream pipe)
|
|
68
|
+
{
|
|
69
|
+
PipeSecurity actual = pipe.GetAccessControl();
|
|
70
|
+
AuthorizationRuleCollection rules = actual.GetAccessRules(true, true, typeof(SecurityIdentifier));
|
|
71
|
+
if (rules.Count != 1)
|
|
72
|
+
throw new InvalidOperationException("created pipe DACL has an unexpected number of ACEs");
|
|
73
|
+
PipeAccessRule rule = rules[0] as PipeAccessRule;
|
|
74
|
+
if (rule == null || rule.AccessControlType != AccessControlType.Allow ||
|
|
75
|
+
!UserSid.Equals(rule.IdentityReference) || rule.PipeAccessRights != PipeAccessRights.FullControl ||
|
|
76
|
+
rule.InheritanceFlags != InheritanceFlags.None || rule.PropagationFlags != PropagationFlags.None ||
|
|
77
|
+
rule.IsInherited)
|
|
78
|
+
throw new InvalidOperationException("created pipe DACL is not exactly current-user full control");
|
|
79
|
+
if (!actual.AreAccessRulesProtected)
|
|
80
|
+
throw new InvalidOperationException("created pipe DACL is not protected from inherited ACEs");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static NamedPipeServerStream CreatePipe(bool first)
|
|
84
|
+
{
|
|
85
|
+
PipeOptions options = PipeOptions.Asynchronous;
|
|
86
|
+
string[] names = Enum.GetNames(typeof(PipeOptions));
|
|
87
|
+
if (first && names.Contains("FirstPipeInstance"))
|
|
88
|
+
options |= (PipeOptions)Enum.Parse(typeof(PipeOptions), "FirstPipeInstance");
|
|
89
|
+
|
|
90
|
+
int instances = 254;
|
|
91
|
+
Type aclFactory = Type.GetType("System.IO.Pipes.NamedPipeServerStreamAcl, System.IO.Pipes.AccessControl", false);
|
|
92
|
+
if (aclFactory != null)
|
|
93
|
+
{
|
|
94
|
+
MethodInfo method = aclFactory.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
|
95
|
+
.Where(m => m.Name == "Create")
|
|
96
|
+
.FirstOrDefault(m => {
|
|
97
|
+
ParameterInfo[] p = m.GetParameters();
|
|
98
|
+
return p.Length >= 8 && p[0].ParameterType == typeof(string) && p[7].ParameterType == typeof(PipeSecurity);
|
|
99
|
+
});
|
|
100
|
+
if (method != null)
|
|
101
|
+
{
|
|
102
|
+
ParameterInfo[] parameters = method.GetParameters();
|
|
103
|
+
object[] args = new object[parameters.Length];
|
|
104
|
+
args[0] = PipeName;
|
|
105
|
+
args[1] = PipeDirection.InOut;
|
|
106
|
+
args[2] = instances;
|
|
107
|
+
args[3] = PipeTransmissionMode.Byte;
|
|
108
|
+
args[4] = options;
|
|
109
|
+
args[5] = 65536;
|
|
110
|
+
args[6] = 65536;
|
|
111
|
+
args[7] = Security;
|
|
112
|
+
for (int i = 8; i < args.Length; i++)
|
|
113
|
+
args[i] = parameters[i].DefaultValue;
|
|
114
|
+
return (NamedPipeServerStream)method.Invoke(null, args);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ConstructorInfo constructor = typeof(NamedPipeServerStream).GetConstructor(new Type[] {
|
|
119
|
+
typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode),
|
|
120
|
+
typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity)
|
|
121
|
+
});
|
|
122
|
+
if (constructor == null)
|
|
123
|
+
throw new PlatformNotSupportedException("this PowerShell runtime has no ACL-capable named pipe constructor");
|
|
124
|
+
return (NamedPipeServerStream)constructor.Invoke(new object[] {
|
|
125
|
+
PipeName, PipeDirection.InOut, instances, PipeTransmissionMode.Byte,
|
|
126
|
+
options, 65536, 65536, Security
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private static bool IsLocalClient(NamedPipeServerStream pipe)
|
|
131
|
+
{
|
|
132
|
+
StringBuilder clientName = new StringBuilder(256);
|
|
133
|
+
uint bufferLengthBytes = (uint)(clientName.Capacity * 2);
|
|
134
|
+
if (!GetNamedPipeClientComputerName(pipe.SafePipeHandle, clientName, bufferLengthBytes))
|
|
135
|
+
{
|
|
136
|
+
// ERROR_PIPE_LOCAL (229), not ERROR_PIPE_NOT_CONNECTED (233),
|
|
137
|
+
// means the client connected through the local NPFS path.
|
|
138
|
+
int error = Marshal.GetLastWin32Error();
|
|
139
|
+
if (error == 229) return true;
|
|
140
|
+
throw new InvalidOperationException("could not verify the named-pipe client computer (Win32 error " + error + ")");
|
|
141
|
+
}
|
|
142
|
+
// A successful lookup identifies a remote SMB client.
|
|
143
|
+
Emit("REJECT\t" + B64("remote named-pipe client computer " + clientName));
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static void Relay(int id, NamedPipeServerStream pipe)
|
|
148
|
+
{
|
|
149
|
+
try
|
|
150
|
+
{
|
|
151
|
+
byte[] buffer = new byte[32768];
|
|
152
|
+
while (!Stopping && pipe.IsConnected)
|
|
153
|
+
{
|
|
154
|
+
int count = pipe.Read(buffer, 0, buffer.Length);
|
|
155
|
+
if (count <= 0) break;
|
|
156
|
+
string data = Convert.ToBase64String(buffer, 0, count);
|
|
157
|
+
Emit("DATA\t" + id + "\t" + data);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (ObjectDisposedException) { }
|
|
161
|
+
catch (IOException) { }
|
|
162
|
+
finally
|
|
163
|
+
{
|
|
164
|
+
NamedPipeServerStream ignored;
|
|
165
|
+
Connections.TryRemove(id, out ignored);
|
|
166
|
+
try { pipe.Dispose(); } catch { }
|
|
167
|
+
Emit("CLOSE\t" + id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private static void AcceptLoop(NamedPipeServerStream first)
|
|
172
|
+
{
|
|
173
|
+
NamedPipeServerStream listener = first;
|
|
174
|
+
try
|
|
175
|
+
{
|
|
176
|
+
AcceptStarted.Wait();
|
|
177
|
+
while (!Stopping)
|
|
178
|
+
{
|
|
179
|
+
listener.WaitForConnection();
|
|
180
|
+
if (!IsLocalClient(listener))
|
|
181
|
+
{
|
|
182
|
+
listener.Dispose();
|
|
183
|
+
listener = CreatePipe(false);
|
|
184
|
+
VerifySecurity(listener);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
NamedPipeServerStream connected = listener;
|
|
189
|
+
listener = CreatePipe(false);
|
|
190
|
+
VerifySecurity(listener);
|
|
191
|
+
int id = Interlocked.Increment(ref NextConnectionId);
|
|
192
|
+
Connections[id] = connected;
|
|
193
|
+
Emit("OPEN\t" + id);
|
|
194
|
+
Task.Run(() => Relay(id, connected));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (ObjectDisposedException error)
|
|
198
|
+
{
|
|
199
|
+
if (!Stopping)
|
|
200
|
+
{
|
|
201
|
+
EmitFatal(error);
|
|
202
|
+
Stopping = true;
|
|
203
|
+
foreach (NamedPipeServerStream pipe in Connections.Values)
|
|
204
|
+
try { pipe.Dispose(); } catch { }
|
|
205
|
+
try { listener.Dispose(); } catch { }
|
|
206
|
+
Environment.ExitCode = 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (Exception error)
|
|
210
|
+
{
|
|
211
|
+
EmitFatal(error);
|
|
212
|
+
Stopping = true;
|
|
213
|
+
foreach (NamedPipeServerStream pipe in Connections.Values)
|
|
214
|
+
try { pipe.Dispose(); } catch { }
|
|
215
|
+
try { listener.Dispose(); } catch { }
|
|
216
|
+
Environment.ExitCode = 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private static void ReadControl()
|
|
221
|
+
{
|
|
222
|
+
string line;
|
|
223
|
+
while (!Stopping && (line = Console.In.ReadLine()) != null)
|
|
224
|
+
{
|
|
225
|
+
string[] fields = line.Split('\t');
|
|
226
|
+
if (fields[0] == "START")
|
|
227
|
+
{
|
|
228
|
+
AcceptStarted.Set();
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (fields.Length < 2) continue;
|
|
232
|
+
int id;
|
|
233
|
+
if (!Int32.TryParse(fields[1], out id)) continue;
|
|
234
|
+
NamedPipeServerStream pipe;
|
|
235
|
+
if (!Connections.TryGetValue(id, out pipe)) continue;
|
|
236
|
+
try
|
|
237
|
+
{
|
|
238
|
+
if (fields[0] == "WRITE" && fields.Length == 3)
|
|
239
|
+
{
|
|
240
|
+
byte[] bytes = Convert.FromBase64String(fields[2]);
|
|
241
|
+
pipe.Write(bytes, 0, bytes.Length);
|
|
242
|
+
pipe.Flush();
|
|
243
|
+
}
|
|
244
|
+
else if (fields[0] == "CLOSE")
|
|
245
|
+
{
|
|
246
|
+
pipe.Dispose();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (ObjectDisposedException) { }
|
|
250
|
+
catch (IOException) { }
|
|
251
|
+
}
|
|
252
|
+
Stopping = true;
|
|
253
|
+
foreach (NamedPipeServerStream pipe in Connections.Values)
|
|
254
|
+
try { pipe.Dispose(); } catch { }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public static void Run(string pipeName)
|
|
258
|
+
{
|
|
259
|
+
PipeName = pipeName;
|
|
260
|
+
UserSid = WindowsIdentity.GetCurrent().User;
|
|
261
|
+
if (UserSid == null) throw new InvalidOperationException("current Windows user SID is unavailable");
|
|
262
|
+
Security = BuildSecurity(UserSid);
|
|
263
|
+
NamedPipeServerStream first = CreatePipe(true);
|
|
264
|
+
VerifySecurity(first);
|
|
265
|
+
string sddl = first.GetAccessControl().GetSecurityDescriptorSddlForm(AccessControlSections.Access);
|
|
266
|
+
Emit("READY\t" + UserSid.Value + "\t" + B64(sddl));
|
|
267
|
+
Task accept = Task.Run(() => AcceptLoop(first));
|
|
268
|
+
ReadControl();
|
|
269
|
+
Stopping = true;
|
|
270
|
+
try { first.Dispose(); } catch { }
|
|
271
|
+
try { accept.Wait(1000); } catch { }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
'@
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
Add-Type -TypeDefinition $source -ErrorAction Stop
|
|
278
|
+
[HqdWindowsPipeHost]::Run($env:HQD_PIPE_NAME)
|
|
279
|
+
} catch {
|
|
280
|
+
[Console]::Error.WriteLine($_.Exception.GetBaseException().Message)
|
|
281
|
+
exit 1
|
|
282
|
+
}
|
|
@@ -18,6 +18,6 @@ type RefreshSession = (options?: {
|
|
|
18
18
|
minValiditySeconds?: number;
|
|
19
19
|
}) => ReturnType<typeof refreshCachedSession>;
|
|
20
20
|
export declare function runAuthRefresh(refreshSession?: RefreshSession): Promise<number>;
|
|
21
|
-
export declare function startAuthRefresh(acquireLease?: typeof acquireCurrentPackageUseLease): Promise<number>;
|
|
21
|
+
export declare function startAuthRefresh(acquireLease?: typeof acquireCurrentPackageUseLease, updateRequestPending?: () => boolean): Promise<number>;
|
|
22
22
|
export {};
|
|
23
23
|
//# sourceMappingURL=hq-auth-refresh.d.ts.map
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// MUST be first: guard the Node version before any dependency that needs a
|
|
14
14
|
// Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
|
|
15
15
|
import "../node-preflight.js";
|
|
16
|
-
import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
|
|
16
|
+
import { acquireCurrentPackageUseLease, currentPackageUseUpdatePending, } from "../utils/package-use-lease.js";
|
|
17
17
|
import "../node-network-compat.js";
|
|
18
18
|
import { isMainModule } from "../utils/is-main-module.js";
|
|
19
19
|
const MIN_VALIDITY_FLAG = "--min-validity-seconds";
|
|
@@ -93,7 +93,7 @@ function packageUseErrorClass(error) {
|
|
|
93
93
|
}
|
|
94
94
|
return typeof value.name === "string" ? value.name : "unknown";
|
|
95
95
|
}
|
|
96
|
-
export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLease) {
|
|
96
|
+
export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLease, updateRequestPending = currentPackageUseUpdatePending) {
|
|
97
97
|
try {
|
|
98
98
|
acquireLease();
|
|
99
99
|
}
|
|
@@ -102,6 +102,15 @@ export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLe
|
|
|
102
102
|
process.stderr.write(`hq-auth-refresh: ${error instanceof Error ? error.message : "The HQ CLI package is being updated"}\n`);
|
|
103
103
|
return 1;
|
|
104
104
|
}
|
|
105
|
+
try {
|
|
106
|
+
if (updateRequestPending()) {
|
|
107
|
+
process.stderr.write("hq-auth-refresh: The HQ CLI package is being updated; retry after the update finishes\n");
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// A failed pending-request probe preserves the existing fail-open behavior.
|
|
113
|
+
}
|
|
105
114
|
process.stderr.write(`hq-auth-refresh: package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
|
|
106
115
|
}
|
|
107
116
|
const { initSentry } = await import("../sentry.js");
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
* a feature that simply is not turned on.
|
|
28
28
|
*/
|
|
29
29
|
import chalk from "chalk";
|
|
30
|
+
import { diagnosticsEnabled } from "../lib/diagnostics.js";
|
|
30
31
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
32
|
+
import { isCompanySelectionError } from "../utils/company-selection-error.js";
|
|
31
33
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
32
34
|
/** The request kind this command files. Matches hq-pro's contract. */
|
|
33
35
|
export const PLAN_RESOURCE = "plan";
|
|
@@ -45,6 +47,20 @@ export const HELD_FOR_CARD_SENTENCE = "Your company said yes and is adding a car
|
|
|
45
47
|
class SponsorError extends Error {
|
|
46
48
|
expected = true;
|
|
47
49
|
}
|
|
50
|
+
function isExpectedCompanyResolveFailure(error) {
|
|
51
|
+
if (isCompanySelectionError(error))
|
|
52
|
+
return true;
|
|
53
|
+
return (typeof error === "object" &&
|
|
54
|
+
error !== null &&
|
|
55
|
+
"expected" in error &&
|
|
56
|
+
error.expected === true);
|
|
57
|
+
}
|
|
58
|
+
function noteSponsorCompanyUnresolved(error) {
|
|
59
|
+
if (isExpectedCompanyResolveFailure(error) || !diagnosticsEnabled())
|
|
60
|
+
return;
|
|
61
|
+
const errorName = error instanceof Error ? error.name : "Error";
|
|
62
|
+
console.error(`billing-sponsor company resolve failed errorName=${errorName}`);
|
|
63
|
+
}
|
|
48
64
|
function fail(err) {
|
|
49
65
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
50
66
|
process.exit(1);
|
|
@@ -185,7 +201,10 @@ export function registerBillingSponsorCommands(billing) {
|
|
|
185
201
|
// this feature is for often belongs to no company at all, and they
|
|
186
202
|
// still need to be told what to do next.
|
|
187
203
|
const named = opts.company ?? billing.opts().company;
|
|
188
|
-
const companyUid = await getCompanyUid(token, named).catch(() =>
|
|
204
|
+
const companyUid = await getCompanyUid(token, named).catch((error) => {
|
|
205
|
+
noteSponsorCompanyUnresolved(error);
|
|
206
|
+
return null;
|
|
207
|
+
});
|
|
189
208
|
// The admin queue is the richer view. A non-admin is refused it, so we
|
|
190
209
|
// fall back to the member SELF-READ — a different route with a
|
|
191
210
|
// different answer: hq-pro returns ONE status for that person and that
|
|
@@ -15,6 +15,8 @@ export type DeriveTriggerRunResult = {
|
|
|
15
15
|
stdout: Buffer;
|
|
16
16
|
stderr: Buffer;
|
|
17
17
|
};
|
|
18
|
+
export declare function resolveWindowsFilesystemPath(candidate: string, cwd: string, resolveRealpath?: (value: string) => string, isDirectory?: (value: string) => boolean): string | undefined;
|
|
19
|
+
export declare function commandOnPath(command: string, env: Record<string, string | undefined>, platform?: NodeJS.Platform): boolean;
|
|
18
20
|
/** Run the native handler contract, retaining exact bytes for differential tests. */
|
|
19
21
|
export declare function runDeriveTriggerFacts(options: DeriveTriggerRunOptions): DeriveTriggerRunResult;
|
|
20
22
|
export declare function runDeriveTriggerFactsFast(argv: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined): number;
|
|
@@ -4,7 +4,6 @@ import * as path from "node:path";
|
|
|
4
4
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
5
5
|
import { isByteOrientedLocale } from "../lib/core-utils/byte-oriented-locale.js";
|
|
6
6
|
export const DERIVE_TRIGGER_FACTS_ASSET = "core/scripts/derive-trigger-facts.sh";
|
|
7
|
-
const TEXT_FACT_AWK = "core/scripts/lib/trigger-fact-text.awk";
|
|
8
7
|
const TRANSCRIPT_TAIL_MAX_BYTES = 1_048_576;
|
|
9
8
|
function effectiveEnvironment(options) {
|
|
10
9
|
return {
|
|
@@ -62,13 +61,51 @@ function globSegmentMatches(pattern, value) {
|
|
|
62
61
|
return false;
|
|
63
62
|
}
|
|
64
63
|
}
|
|
64
|
+
export function resolveWindowsFilesystemPath(candidate, cwd, resolveRealpath = (value) => fs.realpathSync.native(value), isDirectory = (value) => fs.statSync(value).isDirectory()) {
|
|
65
|
+
const value = candidate || ".";
|
|
66
|
+
// Node's win32 path helpers collapse `link/..` lexically before the OS can
|
|
67
|
+
// follow `link`. Bash walks the path component by component, so resolve a
|
|
68
|
+
// symlink before applying each parent component while keeping the emitted
|
|
69
|
+
// glob word untouched.
|
|
70
|
+
// Git Bash exposes mounted Windows drives as /c/... while Node's win32
|
|
71
|
+
// path parser treats that spelling as a path rooted at the current drive.
|
|
72
|
+
// Convert only the drive-mount prefix before probing the filesystem.
|
|
73
|
+
const windowsValue = value.replace(/^\/([A-Za-z])(?:\/|$)/, (_match, drive) => `${drive.toUpperCase()}:\\`);
|
|
74
|
+
const absolute = path.win32.isAbsolute(windowsValue);
|
|
75
|
+
const parsed = path.win32.parse(absolute ? windowsValue : cwd);
|
|
76
|
+
let current = absolute ? parsed.root : cwd;
|
|
77
|
+
const rest = absolute ? windowsValue.slice(parsed.root.length) : windowsValue;
|
|
78
|
+
for (const segment of rest.split(/[\\/]+/)) {
|
|
79
|
+
if (!segment || segment === ".")
|
|
80
|
+
continue;
|
|
81
|
+
if (segment === "..") {
|
|
82
|
+
try {
|
|
83
|
+
current = resolveRealpath(current);
|
|
84
|
+
if (!isDirectory(current))
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
current = path.win32.dirname(current);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
current = path.win32.join(current, segment);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return current;
|
|
97
|
+
}
|
|
65
98
|
function filesystemPath(candidate, cwd) {
|
|
66
99
|
const value = candidate || ".";
|
|
67
|
-
|
|
100
|
+
if (process.platform !== "win32") {
|
|
101
|
+
return path.isAbsolute(value) ? value : `${cwd}${path.sep}${value}`;
|
|
102
|
+
}
|
|
103
|
+
return resolveWindowsFilesystemPath(value, cwd);
|
|
68
104
|
}
|
|
69
105
|
function isDirectoryPath(candidate, cwd) {
|
|
70
106
|
try {
|
|
71
|
-
|
|
107
|
+
const resolved = filesystemPath(candidate, cwd);
|
|
108
|
+
return resolved !== undefined && fs.statSync(resolved).isDirectory();
|
|
72
109
|
}
|
|
73
110
|
catch {
|
|
74
111
|
return false;
|
|
@@ -76,7 +113,10 @@ function isDirectoryPath(candidate, cwd) {
|
|
|
76
113
|
}
|
|
77
114
|
function pathEntryExists(candidate, cwd) {
|
|
78
115
|
try {
|
|
79
|
-
|
|
116
|
+
const resolved = filesystemPath(candidate, cwd);
|
|
117
|
+
if (resolved === undefined)
|
|
118
|
+
return false;
|
|
119
|
+
fs.lstatSync(resolved);
|
|
80
120
|
return true;
|
|
81
121
|
}
|
|
82
122
|
catch {
|
|
@@ -86,16 +126,17 @@ function pathEntryExists(candidate, cwd) {
|
|
|
86
126
|
function expandBashGlob(token, cwd) {
|
|
87
127
|
if (!hasGlobPattern(token))
|
|
88
128
|
return [token];
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
129
|
+
// Bash emits slash-separated words even when running under Git Bash on Windows.
|
|
130
|
+
const absolute = token.startsWith("/");
|
|
131
|
+
const trailingSlash = token.endsWith("/");
|
|
132
|
+
const parts = token.split("/").filter((part, index) => !(absolute && index === 0));
|
|
92
133
|
if (trailingSlash)
|
|
93
134
|
parts.pop();
|
|
94
|
-
let candidates = [absolute ?
|
|
135
|
+
let candidates = [absolute ? "/" : ""];
|
|
95
136
|
let matchedPattern = false;
|
|
96
137
|
for (const [index, part] of parts.entries()) {
|
|
97
138
|
if (!hasGlobPattern(part)) {
|
|
98
|
-
candidates = candidates.map((parent) => !parent ? part : parent ===
|
|
139
|
+
candidates = candidates.map((parent) => !parent ? part : parent === "/" ? `/${part}` : `${parent}/${part}`);
|
|
99
140
|
if (matchedPattern) {
|
|
100
141
|
const needsDirectory = index < parts.length - 1 || trailingSlash;
|
|
101
142
|
candidates = candidates.filter((candidate) => needsDirectory ? isDirectoryPath(candidate, cwd) : pathEntryExists(candidate, cwd));
|
|
@@ -106,6 +147,8 @@ function expandBashGlob(token, cwd) {
|
|
|
106
147
|
const expanded = [];
|
|
107
148
|
for (const parent of candidates) {
|
|
108
149
|
const directory = filesystemPath(parent || ".", cwd);
|
|
150
|
+
if (directory === undefined)
|
|
151
|
+
continue;
|
|
109
152
|
let names;
|
|
110
153
|
try {
|
|
111
154
|
names = fs.readdirSync(directory).sort();
|
|
@@ -118,7 +161,7 @@ function expandBashGlob(token, cwd) {
|
|
|
118
161
|
continue;
|
|
119
162
|
if (!globSegmentMatches(part, name))
|
|
120
163
|
continue;
|
|
121
|
-
expanded.push(!parent ? name : parent ===
|
|
164
|
+
expanded.push(!parent ? name : parent === "/" ? `/${name}` : `${parent}/${name}`);
|
|
122
165
|
}
|
|
123
166
|
}
|
|
124
167
|
if (expanded.length === 0)
|
|
@@ -128,7 +171,7 @@ function expandBashGlob(token, cwd) {
|
|
|
128
171
|
if (trailingSlash) {
|
|
129
172
|
candidates = candidates
|
|
130
173
|
.filter((candidate) => isDirectoryPath(candidate, cwd))
|
|
131
|
-
.map((candidate) => candidate.endsWith(
|
|
174
|
+
.map((candidate) => candidate.endsWith("/") ? candidate : `${candidate}/`)
|
|
132
175
|
.sort();
|
|
133
176
|
}
|
|
134
177
|
return matchedPattern && candidates.length > 0 ? candidates : [token];
|
|
@@ -136,16 +179,27 @@ function expandBashGlob(token, cwd) {
|
|
|
136
179
|
function bashUnquotedWords(facts, cwd) {
|
|
137
180
|
return facts.join(" ").split(/[\t\n ]+/).filter(Boolean).flatMap((word) => expandBashGlob(word, cwd));
|
|
138
181
|
}
|
|
139
|
-
function commandOnPath(command, env) {
|
|
182
|
+
export function commandOnPath(command, env, platform = process.platform) {
|
|
140
183
|
const pathValue = env.PATH ?? "";
|
|
184
|
+
const extensions = platform === "win32"
|
|
185
|
+
? ["", ...(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)]
|
|
186
|
+
: [""];
|
|
141
187
|
for (const directory of pathValue.split(path.delimiter)) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
188
|
+
for (const extension of extensions) {
|
|
189
|
+
const candidate = path.join(directory || process.cwd(), `${command}${extension}`);
|
|
190
|
+
try {
|
|
191
|
+
if (platform === "win32") {
|
|
192
|
+
if (fs.statSync(candidate).isFile())
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// PATH probes intentionally ignore missing and inaccessible entries.
|
|
202
|
+
}
|
|
149
203
|
}
|
|
150
204
|
}
|
|
151
205
|
return false;
|
|
@@ -185,10 +239,12 @@ function parsePayload(raw) {
|
|
|
185
239
|
parsed = JSON.parse(raw);
|
|
186
240
|
}
|
|
187
241
|
catch {
|
|
188
|
-
|
|
242
|
+
// The shell redirects jq's parse diagnostic and leaves every field empty.
|
|
243
|
+
return emptyPayload();
|
|
189
244
|
}
|
|
190
245
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
191
|
-
|
|
246
|
+
// jq's object indexing fails quietly through the same redirected pipeline.
|
|
247
|
+
return emptyPayload();
|
|
192
248
|
}
|
|
193
249
|
const record = parsed;
|
|
194
250
|
const toolResponse = jqField(record.tool_response);
|
|
@@ -207,6 +263,12 @@ function parsePayload(raw) {
|
|
|
207
263
|
}
|
|
208
264
|
return fields;
|
|
209
265
|
}
|
|
266
|
+
function emptyPayload() {
|
|
267
|
+
return {
|
|
268
|
+
tool: "", command: "", runInBackground: "", toolResponse: "", prompt: "",
|
|
269
|
+
cwd: "", sessionId: "", transcriptPath: "",
|
|
270
|
+
};
|
|
271
|
+
}
|
|
210
272
|
function matchTextFacts(rawText, env) {
|
|
211
273
|
const text = localeLower(rawText, env);
|
|
212
274
|
const facts = [];
|
|
@@ -391,9 +453,8 @@ function computeOutput(options) {
|
|
|
391
453
|
const withAssistantIntent = args[1] === "--with-assistant-intent";
|
|
392
454
|
if (args.length === 2 && !withAssistantIntent)
|
|
393
455
|
throw new ShellFallbackRequired("unsupported argv");
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
456
|
+
// The shell accepts unknown event names and emits the baseline `always`
|
|
457
|
+
// fact (plus any static session facts); it does not validate the event.
|
|
397
458
|
const paired = withAssistantIntent && (event === "PreToolUse" || event === "UserPromptSubmit");
|
|
398
459
|
const env = effectiveEnvironment(options);
|
|
399
460
|
const jqAvailable = options.jqAvailable ?? commandOnPath("jq", env);
|
|
@@ -465,6 +526,7 @@ function shellFallback(options) {
|
|
|
465
526
|
args: [...options.argv],
|
|
466
527
|
cwd: options.hqRoot,
|
|
467
528
|
hqRoot: options.hqRoot,
|
|
529
|
+
msysScriptPath: true,
|
|
468
530
|
stdin: options.stdin,
|
|
469
531
|
captureOutput: true,
|
|
470
532
|
env,
|
|
@@ -487,12 +549,17 @@ function missingSupportLibrary(hqRoot) {
|
|
|
487
549
|
],
|
|
488
550
|
];
|
|
489
551
|
for (const [relativePath, message] of required) {
|
|
552
|
+
let descriptor;
|
|
490
553
|
try {
|
|
491
|
-
fs.
|
|
554
|
+
descriptor = fs.openSync(path.join(hqRoot, relativePath), "r");
|
|
492
555
|
}
|
|
493
556
|
catch {
|
|
494
557
|
return message;
|
|
495
558
|
}
|
|
559
|
+
finally {
|
|
560
|
+
if (descriptor !== undefined)
|
|
561
|
+
fs.closeSync(descriptor);
|
|
562
|
+
}
|
|
496
563
|
}
|
|
497
564
|
return undefined;
|
|
498
565
|
}
|
|
@@ -12,6 +12,7 @@ export type MigratePolicyDependencies = {
|
|
|
12
12
|
error?: Error;
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
|
+
export declare function executableInPath(name: string, env: NodeJS.ProcessEnv, fileSystem: typeof fs, platform?: NodeJS.Platform): boolean;
|
|
15
16
|
export declare function runMigratePolicyTriggers(args: readonly string[], deps: MigratePolicyDependencies): {
|
|
16
17
|
code: number;
|
|
17
18
|
stdout: string;
|